diff --git a/.agent/plans/browser_like_layout_invalidation_plan.md b/.agent/plans/browser_like_layout_invalidation_plan.md new file mode 100644 index 000000000..a9cf9b49f --- /dev/null +++ b/.agent/plans/browser_like_layout_invalidation_plan.md @@ -0,0 +1,493 @@ +# Browser-Like Layout Invalidation Plan + +> Status: PROPOSED - roadmap for replacing coarse layout invalidation with typed, +> browser-like dirty propagation in the HTML/RichText layout path. + +## Goal + +Move eepp's HTML and `UIRichText` layout invalidation closer to browser-engine +semantics without giving up the current optimized coalescing behavior that made +large Markdown documents fast again. + +The desired end state is: + +- layout recomputation is proportional to the real dependency that changed, +- child changes preserve enough parent/ancestor dirtiness to be reconciled in the + next layout phase, +- re-entrant invalidations during a layout pass are coalesced instead of causing + parent layout loops, +- asynchronous resource changes, especially image and stylesheet loads, update + final geometry without requiring a later unrelated invalidation, +- `UIRichText` remains usable outside `UIWebView`, +- `UIWebView` can evolve toward an isolated document/scene model without forcing + the same constraints onto normal eepp GUI layouts. + +This is not a request to recompute more aggressively. The whole point is to +retain the fast path while making the skipped work explicit and correct. + +## References + +- WebKit layout basics and dirty-bit model: + https://webkit.org/blog/116/webcore-rendering-iii-layout-basics/ +- Chromium LayoutNG architecture: + https://developer.chrome.com/docs/chromium/layoutng/ +- Browser rendering pipeline overview: + https://web.dev/howbrowserswork/ +- CSS 2 root/body/canvas background special case: + https://www.w3.org/TR/CSS2/colors.html#background +- CSS Visual Formatting Model: + https://www.w3.org/TR/CSS22/visuren.html + +## Current Problem + +The recent Markdown performance regression exposed two opposite failure modes in +the current invalidation model: + +1. Eager parent recomputation can explode into thousands of redundant + `UIRichText` rebuilds while the same layout tree is already being reconciled. +2. Over-coalescing can skip a required ancestor update, leaving visible stale + geometry until some unrelated event, such as resize or hover, invalidates the + layout again. + +The first failure is why rendering `README.md` in `UIMarkdownView` became much +slower than ecode 0.8.0. The second failure appeared with delayed resource +changes: + +- asynchronous image replacement resized a child after the initial HTML layout, +- asynchronous CSS load changed the size of the Hacker News table/body content, +- the affected rich-text/body subtree could become correct locally, +- a required ancestor, especially the root `html` element, could remain stale + until the next unrelated layout invalidation. + +The core problem is not simply "parents are not dirty enough". It is that eepp +mostly tracks layout dirtiness as one coarse `mDirtyLayout` state plus message +bubbling. Browser engines preserve more information: + +- this node itself needs layout, +- a normal-flow child needs layout, +- an out-of-flow positioned child needs layout, +- intrinsic size changed, +- style changed, +- formatting-context contents changed, +- layout can stop at this boundary, +- layout must continue through this ancestor because its used size depends on + the changed descendant. + +Without those distinctions, eepp has to choose between two imprecise behaviors: +invalidating too broadly, or suppressing invalidations that later prove to be +required. + +## Current Bridge Fix + +The targeted `html > body` invalidation is a compatibility bridge, not a generic +layout invalidation strategy. + +The root `html` element has browser-specific responsibilities that ordinary +`UIRichText` containers do not have. In particular, it must contain the body +extent, and CSS has several special root/body propagation rules. When async CSS +or table layout causes the body to grow after the root has already performed its +current layout pass, the root must be marked dirty so the document height is +reconciled on the next dirty-layout flush. + +This bridge is acceptable only because it is scoped to the HTML root/body +contract. It must not become the model for arbitrary rich-text ancestors. A +generic version would recreate the Markdown performance regression by allowing +deep child changes to repeatedly dirty ancestors without understanding the +dependency that changed. + +## Why Browser Engines Avoid This Failure + +Browser engines do not treat layout invalidation as a request to immediately +re-run every interested ancestor. They mark objects dirty and later run layout +from carefully chosen roots. + +Conceptually, a child change in a browser does two things: + +1. It marks the child or formatting object as needing layout. +2. It preserves a child-dirty dependency on ancestors whose used geometry may + depend on that child. + +During the layout phase, the engine decides whether each ancestor must recompute +itself, descend into dirty children, or stop at a relayout boundary. That +decision is based on formatting context, containment, positioning, intrinsic +sizing, and style dependencies. + +eepp does something similar at a high level with dirty-layout queues and +coalescing, but it lacks the typed dependency state needed to make the same +decision reliably. `UIRichText` makes this harder because it owns a parallel +formatted text stream built from child widgets. A descendant can change in a way +that invalidates: + +- only its own widget geometry, +- the parent rich-text stream, +- the intrinsic size exposed by the rich-text owner, +- an ancestor block/table/flex layout, +- the document root extent. + +Those are different invalidation scopes, but today they often flow through the +same parent layout-attribute message. + +## Constraints + +- Do not restore eager re-entrant parent recomputation. +- Do not make every rich-text child resize dirty every ancestor. +- Do not add tag-specific layout fixes where the behavior belongs to a generic + CSS/layout concept. +- Keep `UIRichText` valid as a standalone widget, independent of `UIWebView`. +- Let the future per-`UIWebView` `UISceneNode` branch become a clean document + boundary, but do not require that branch before fixing generic invalidation. +- Preserve the existing no-heap-allocation intent in hot dirty-layout queues. + Reusable snapshots or `SmallVector`-style storage are preferred over + allocating per invalidation flush. + +## Proposed Architecture + +Introduce typed layout dirtiness and propagate only the dependency that actually +changed. + +Exact names can change, but the model should distinguish at least: + +```cpp +enum class LayoutDirtyReason : Uint32 { + None = 0, + SelfLayout = 1 << 0, + NormalChildLayout = 1 << 1, + OutOfFlowChildLayout = 1 << 2, + IntrinsicSize = 1 << 3, + Style = 1 << 4, + FormattingContext = 1 << 5, + DocumentExtent = 1 << 6, +}; +``` + +The state should live near `UILayout` / `UIHTMLWidget` boundaries rather than in +ad-hoc element code. HTML-specific reasons can be layered on top of the generic +bits when needed. + +## Propagation Rules + +### Self Layout + +Use when the widget's own used position, size, padding, margin, border, or style +state requires recalculation. + +Propagation: + +- enqueue the widget or nearest layout root, +- notify parent only if the parent's layout depends on this widget's used size + or position. + +### Normal-Flow Child Layout + +Use when an in-flow descendant changed and the parent may need to reconcile child +geometry during its next layout. + +Propagation: + +- preserve a child-dirty bit on ancestors until the layout phase consumes it, +- stop at formatting-context boundaries that can prove the ancestor's exposed + size is unchanged, +- continue upward when intrinsic size or auto size may change. + +### Out-Of-Flow Child Layout + +Use for `position: absolute` and `position: fixed` descendants. + +Propagation: + +- dirty the containing block or fixed-position root for placement, +- do not dirty normal-flow ancestors for auto-size contribution, because + out-of-flow boxes do not affect normal-flow sizing. + +### Intrinsic Size + +Use when a widget's min-content, max-content, preferred, or auto-size +contribution changes. + +Examples: + +- image natural size appears after async load, +- text/font metrics change, +- rich-text wrapping changes exposed min/max width, +- table column intrinsic widths change. + +Propagation: + +- propagate through ancestors that use intrinsic size, +- avoid propagating through containers with definite sizes when the change cannot + affect their used size. + +### Formatting Context + +Use when the internal formatting stream of a context is stale. + +Examples: + +- `UIRichText::rebuildRichText()` contents changed, +- inline text span style affects line metrics, +- custom block size affects line wrapping, +- float/clear participation changes. + +Propagation: + +- dirty the formatting-context owner first, +- notify ancestors only after the owner knows whether its exposed size changed, +- avoid synchronous parent recomputation while the owner is already updating its + own layout tree. + +### Document Extent + +Use for document-root invariants, not for generic widget layout. + +Examples: + +- HTML root must contain body content extent, +- viewport/document scrollable height changed, +- root/body special CSS propagation affected document metrics. + +Propagation: + +- route through `UIWebView` document root or the future per-document + `UISceneNode`, +- keep this bit out of normal eepp GUI containers unless they explicitly opt + into document-like behavior. + +## RichText-Specific Direction + +`UIRichText` should be treated as a formatting-context owner. + +When a descendant changes: + +1. If the descendant is in normal flow and participates in the rich-text stream, + mark the nearest `UIRichText` formatting context dirty. +2. During the next layout pass, rebuild/reflow that context once. +3. Compare the context's exposed geometry/intrinsic size before notifying + ancestors. +4. If the context is already updating its layout tree, coalesce the descendant + notification into the active pass instead of recursively recomputing the + parent. + +This preserves the successful Markdown optimization: thousands of inline/block +children can settle into one owner-level rich-text rebuild sequence instead of +causing repeated ancestor rebuilds. + +The missing piece is that the coalesced dirty reason must survive until the +active layout phase can decide whether ancestor geometry changed. A boolean +"skip because updating" guard is too weak unless it stores the reason that was +skipped. + +## Table-Specific Direction + +Tables are high-risk because a local change can affect: + +- cell intrinsic widths, +- column width distribution, +- row heights, +- table wrapper size, +- containing block size, +- document extent. + +`TableLayouter` should expose explicit invalidation results after layout: + +- no exposed size change, +- block-axis size changed, +- inline-axis intrinsic width changed, +- both axes changed. + +Ancestors should react to that result instead of guessing from generic child +messages. This matters for real pages such as Hacker News, where the page height +is effectively the height of one large table after async CSS applies. + +## WebView And Document Boundaries + +The branch where each `UIWebView` owns its own `UISceneNode` is the right long +term direction for HTML documents. + +Benefits: + +- document dirty queues are isolated from the normal eepp GUI scene, +- root/body/viewport special behavior has a clear owner, +- resource loading can target a document lifecycle, +- document memory and cached layout state can be released as a unit, +- future stacking context, paint invalidation, and scrollable viewport behavior + can live in the document scene instead of leaking into generic widgets. + +This should not remove standalone `UIRichText`. The intended split is: + +- `UIRichText`: reusable formatting-context widget for eepp GUI and embedded + HTML-like content. +- `UIWebView`: document host with browser-like root/body/viewport/resource + semantics. +- shared layouters: block, inline, table, flex, grid, and positioning logic used + by both when applicable. + +## Implementation Phases + +Before starting the phases, keep a persistent rollback checkpoint workflow: + +- after each phase is working and validated, create a named `git stash` copy for + that phase, +- treat these stashes as archival safety snapshots, not temporary working + stashes, +- do not drop or pop the checkpoint stash after recovering from it; use + non-destructive recovery, such as applying it to a scratch branch or copying + the relevant diff, +- include the phase number, topic, and validation status in the stash message, + for example `phase-2 typed-dirty-state validated-markdown-hn`, +- create a fresh checkpoint for every materially different phase result, so + later experiments can roll back to the last known-good implementation without + losing intermediate progress. + +### Phase 1: Instrumentation Guardrails + +Keep the benchmark and counters that exposed the regression. + +Required metrics: + +- total dirty-layout invalidations, +- rich-text rebuild count, +- layout-tree update count, +- dirty flush wall time, +- Markdown README benchmark total time. + +Required fixtures: + +- `Benchmark.MarkdownReadme`, +- async image resize in a rich-text ancestor, +- async CSS load for Hacker News-like table/body/root sizing, +- focused table/body/html height invariant. + +The benchmark should remain cheap enough to run during layout work and should +fail loudly if a "correctness" change silently restores thousands of redundant +rebuilds. + +### Phase 2: Add Typed Dirty State + +Add compact dirty-reason bits to the layout path. + +Requirements: + +- no heap allocation per invalidation, +- queue coalescing remains stable, +- invalidations generated during a flush are preserved for the next flush + iteration, +- existing `setLayoutDirty()` can initially map to `SelfLayout` for + compatibility, +- new code paths use more precise reasons where known. + +Start by plumbing the state without changing behavior. Tests and benchmarks +should remain equivalent to the current optimized implementation. + +### Phase 3: Convert RichText Notifications + +Convert `UIRichText`, `UITextSpan`, and replaced inline/custom blocks to emit +formatting-context and intrinsic-size dirty reasons. + +Important cases: + +- text content changes, +- inline style changes that affect metrics, +- custom block size changes, +- image natural size changes after async load, +- min/max width queries, +- active `UIRichText::updateLayoutTree()` coalescing. + +The active-layout guard should record skipped dirty reasons instead of merely +returning. At the end of the current layout pass, the owner can decide whether +its exposed size changed and whether parent propagation is required. + +### Phase 4: Convert HTML Block/Table Boundaries + +Teach block and table layouters to report the external effect of their layout +pass. + +At minimum: + +- child geometry changed but container size did not, +- container block-size changed, +- container inline-size/intrinsic width changed, +- document extent may have changed. + +Use those results to replace broad parent dirtying with targeted propagation. + +### Phase 5: Make UIWebView A Document Boundary + +After the per-`UIWebView` scene branch is merged or ready, move root/body, +viewport, resource-loading, and document extent invalidation behind that +boundary. + +Expected changes: + +- root/body bridge becomes document-root logic, +- async CSS/image invalidation targets the document scene, +- dirty layout queues for HTML documents are isolated from normal UI scene + queues, +- document-level metrics can be flushed after resource completion without + touching unrelated GUI widgets. + +### Phase 6: Remove Compatibility Bridges + +Once typed invalidation and document boundaries cover the root/body async cases, +revisit the special `html > body` bridge. + +It can be removed only when tests prove: + +- async CSS updates root height correctly, +- delayed image load updates document height correctly, +- normal standalone `UIRichText` resizing works, +- Markdown benchmark remains near the optimized baseline, +- the sensitive layout tests continue passing. + +## Testing Plan + +Keep and expand these tests: + +- `Benchmark.MarkdownReadme` + - validates rebuild counts and timing, + - protects against re-entrant parent recomputation regressions. +- `UIHTML.HtmlContainsTableBodyHeight` + - validates root/body/table height after async stylesheet application. +- async image resize test + - creates an image/custom block with initially unknown size, + - resizes it after initial layout, + - asserts rich-text and ancestor geometry update without viewport resize. +- standalone `UIRichText` resize test + - ensures the fix is not only `UIWebView`-specific. +- out-of-flow child resize test + - verifies absolute/fixed descendants do not dirty normal-flow auto size. +- existing sensitive tests: + - `GridContainer.newsblurReducedGrid`, + - `UIHTML.ContactFormLayout`, + - `UIRichText.MinMaxWidthChildren`. + +When adding tests, prefer invariants over pixel-perfect snapshots: + +- parent contains child bottom edge, +- body/html contain table/body content, +- intrinsic min/max width changes are reflected, +- fixed-size ancestors are not unnecessarily recomputed, +- benchmark counters stay under known thresholds. + +## Risks + +- A typed invalidation system can become more complicated than the current bug if + the bits are too granular or inconsistently applied. +- RichText is both a widget and a formatting-context bridge. It must not leak + browser-only document concepts into normal eepp GUI usage. +- Tables can invalidate both intrinsic inline size and final block size. Treating + them as a normal block child will miss real dependencies. +- Definite-size containers need careful handling: they may need child placement + updates without needing parent size propagation. +- Performance can regress silently if correctness tests do not also track + recomputation counts. + +## Near-Term Guidance + +For current fixes before the full architecture lands: + +- Prefer narrowly scoped invalidation at the formatting-context owner. +- Preserve dirty reasons generated during an active layout pass. +- Do not synchronously recompute parents from inside a child layout update. +- Compare exposed geometry before notifying ancestors. +- Keep document-root fixes scoped to document-root invariants. +- Validate every correctness fix with the Markdown benchmark counters. diff --git a/.agent/plans/ui_inline_formatting_context_plan.md b/.agent/plans/ui_inline_formatting_context_plan.md deleted file mode 100644 index 3dc2ad207..000000000 --- a/.agent/plans/ui_inline_formatting_context_plan.md +++ /dev/null @@ -1,943 +0,0 @@ -# UI Inline Formatting Context Plan - -## Goal - -Introduce a UI-specialized inline formatting path that can lay out HTML/UI inline -content directly from the existing `Node` / `UIWidget` / `UITextNode` tree, -without first reconstructing a parallel `Graphics::RichText::InlineItem` tree. - -The current first-class inline boxes implementation is functionally correct and -must remain the baseline. This plan is about reducing duplicated data, improving -hot-path allocation behavior, and separating the generic `Graphics::RichText` -frontend from the browser-oriented UI frontend. - -The desired end state is: - -- `Graphics::RichText` remains a generic graphics primitive. -- The current `RichText::InlineItem` path remains valid for non-UI callers. -- UI/HTML inline layout can consume the existing UI node tree directly. -- Shared inline layout logic is reused instead of duplicating line-breaking, - baseline, float, selection, and fragment behavior. -- UI fragments reference source UI nodes and drawables instead of copying large - style/widget state into a mirrored RichText tree. -- The migration is incremental, with the existing RichText path available as the - verified fallback until each UI phase is proven. - -## Current Problem - -The current UI pipeline is: - -1. `UIRichText::rebuildRichText()` walks the UI child tree. -2. It builds a parallel `Graphics::RichText::mInlineItems` tree. -3. `RichTextInlineLayouter` flattens that tree into layout runs. -4. Layout produces `RenderParagraph`, `RenderSpan`, and `InlineFragment` data. -5. `BlockLayouter::positionRichTextChildren()` groups fragments by source node - and maps geometry back to UI widgets/text nodes. -6. Drawing uses `RenderSpan` for text/atomic payloads and `InlineFragment` for - inline box backgrounds, borders, decorations, and hit boxes. - -This is acceptable for a generic rich text widget. For browser-like HTML it is -less attractive because: - -- The UI node tree already exists and already stores most CSS/widget state. -- The RichText inline tree duplicates source hierarchy. -- Several fields are copied or bridged from UI to Graphics only to be mapped - back to UI later. -- Some data is carried through several structures: - - `UIWidget` / `UITextNode` - - `RichText::InlineItem` - - `InlineLayoutRun` - - `RenderSpan` - - `InlineFragment` - - `BlockLayouter::FragmentBucket` -- Background/border fidelity has required more bridge fields, for example - drawable pointers and fragment-color override flags. -- Allocation pressure remains visible in vectors, text objects, fragment lists, - and repeated temporary structures. - -## Non-Goals - -- Do not make `Graphics::RichText` depend on UI types. -- Do not remove `Graphics::RichText::InlineItem` in this project phase. -- Do not rewrite the whole UI layout engine in one pass. -- Do not regress drawing, selection string, hit testing, selection rectangles, - inline background/border painting, line-height, vertical-align, floats, tables, - details/summary, forms, or invalid-width performance. -- Do not reintroduce legacy `RichText::Block`, `SpanBlock`, `CustomBlock`, - `getBlocks()`, or `mBlocks`. -- Do not add compatibility layers for deleted RichText block APIs. - -## Architectural Direction - -Split the inline formatting engine into two conceptual layers: - -1. A storage-agnostic inline layout core. -2. One or more frontends/providers that expose inline items to that core. - -The initial frontends should be: - -- `GraphicsRichTextInlineProvider` - - Reads `Graphics::RichText::mInlineItems`. - - Preserves current generic RichText behavior. -- `UIInlineProvider` - - Walks `Node` / `UIWidget` / `UITextNode` children directly. - - Resolves CSS/UI metrics from existing widgets. - - Produces UI-oriented fragments that reference source nodes. - -The layout core should not know whether items came from `RichText::InlineItem` or -the UI tree. It should operate on a compact item view/cursor API. - -## Proposed Types - -Exact names can change, but ownership boundaries should remain. - -### Inline Item View - -The shared layout core needs a lightweight, non-owning view of the current inline -item. - -Conceptual shape: - -```cpp -struct InlineItemView { - enum class Type { TextRun, BoxStart, BoxEnd, AtomicBox, LineBreak }; - - Type type; - InlineSourceId source; - InlineStyleId style; - InlineBoxMetrics box; - InlineTextRun text; - InlineAtomicMetrics atomic; - InlineFloat floatType; - InlineClear clearType; - BaselineAlignValue baselineAlign; -}; -``` - -Important requirements: - -- The view must not own strings, widgets, text objects, or child vectors. -- The view can be invalidated by advancing the provider cursor. -- The layout core may copy only the small fields needed for output fragments. -- Source identity must be stable enough to map layout results back to nodes. - -### Inline Provider - -The provider exposes the inline content stream in tree order. - -Conceptual API: - -```cpp -class InlineProvider { - public: - void reset(); - bool next( InlineItemView& out ); - FontStyleConfig resolveTextStyle( InlineStyleId style ) const; - String::View text( const InlineTextRun& run ) const; - Sizef atomicSize( const InlineAtomicMetrics& atomic ) const; - Float atomicBaseline( const InlineAtomicMetrics& atomic ) const; -}; -``` - -The API may be implemented as templates instead of virtual calls if profiling -shows virtual dispatch overhead matters. Start with the simplest design that -keeps the storage boundary clean. - -### UI Source Identity - -Avoid `void*` proliferation in new UI-specific structures. - -Use a small typed source handle: - -```cpp -struct UISourceRef { - enum class Type { None, TextNode, Widget }; - Type type{ Type::None }; - Node* node{ nullptr }; -}; -``` - -The generic `Graphics::RichText` path can keep `InlineSource` as it exists today. -The UI path can use typed UI source refs internally. - -### Layout Output - -The shared core should produce storage-neutral line results, but the UI path -should avoid copying full widget metadata into fragments. - -Conceptual output: - -```cpp -struct InlineLineBox { - Float y; - Float height; - Float baseline; - Float width; - SmallVector fragments; -}; - -struct InlineFragmentRef { - enum class Type { TextRun, Box, AtomicBox }; - Type type; - UISourceRef source; - Rectf bounds; - Rectf paintBounds; - Int64 startCharIndex; - Int64 endCharIndex; - bool startsInlineBox; - bool endsInlineBox; - InlineFragmentPaint paint; -}; -``` - -`InlineFragmentPaint` should be compact. For the UI path, prefer references to -existing UI drawables/styles rather than copied values. - -## UI-Specific Implementation Strategy - -### 1. Keep The Current RichText Path As Baseline - -Before introducing the UI-specific path: - -- Preserve the current `UIRichText::rebuildRichText()` path. -- Keep all current tests passing. -- Add any missing regression coverage for behavior discovered during the review: - - background color plus border radius on inline anchors, - - background images on inline boxes, - - split inline background/border fragments, - - nested inline box hit boxes. - -The first UI-specific implementation should be hidden behind a feature flag or -internal switch so test comparisons can run both paths. - -Possible flag: - -```cpp -enum class InlineLayoutBackend { - GraphicsRichText, - UINodeTree -}; -``` - -Default must remain the current backend until the UI path reaches parity. - -### 2. Extract Layout Core From `RichTextInlineLayouter` - -Move the reusable algorithms out of the private implementation that directly -depends on `std::vector`. - -Candidate responsibilities to extract: - -- text run measurement, -- text wrapping, -- line construction, -- float placement, -- baseline alignment, -- line metric recomputation, -- fragment reconstruction, -- first/last inline box edge detection, -- selection/hit-test geometry helpers. - -Do not extract everything at once. Start with a low-risk seam: - -1. Build layout runs from a provider. -2. Keep the rest of `RichTextInlineLayouter` unchanged. -3. Prove that the RichText provider produces byte-for-byte equivalent output for - current tests. - -### 3. Introduce `GraphicsRichTextInlineProvider` - -This provider adapts existing `RichText::InlineItem` data to the new provider -API. - -Acceptance criteria: - -- No behavior changes. -- `RichText.*` passes. -- `UIRichText.*` passes through the existing RichText backend. -- No new UI includes in `Graphics::RichText`. - -This phase proves the provider abstraction without changing UI behavior. - -### 4. Introduce `UIInlineProvider` - -The UI provider should walk the actual children of the `UIRichText` container. - -Responsibilities: - -- Traverse text nodes and inline widgets in tree order. -- Apply the same whitespace collapsing rules as current - `UIRichText::rebuildRichText()`. -- Represent true inline widgets as box start/end items. -- Represent inline-blocks, replaced elements, floats, controls, list markers, - and line breaks as atomic items. -- Skip invisible nodes. -- Skip out-of-flow descendants where current layout does. -- Resolve margins, padding, borders, background, text decoration, line-height, - baseline alignment, float, and clear from existing `UIWidget` state. -- Keep source references as `UITextNode*` / `UIWidget*`. - -Important: the UI provider must not allocate a mirror tree. - -The provider may keep a traversal stack. That stack should use `SmallVector` and -contain only node pointers and small state: - -```cpp -struct UITraversalFrame { - Node* node; - Node* nextChild; - UIWidget* inlineBox; - bool emittedStart; -}; -``` - -### 5. Shared Whitespace Collapsing - -Whitespace collapsing is currently embedded in `UIRichText::rebuildRichText()`. -The UI provider needs equivalent behavior without materializing a copied text -tree. - -Plan: - -- Extract whitespace state into a small helper: - -```cpp -struct InlineWhitespaceState { - bool shouldCollapse; - bool lastRunEndsWithSpace; - bool atBlockBoundary; -}; -``` - -- For each text node, produce a `String::View` or a compact transformed buffer. -- Avoid allocating a new `String` when no trimming/collapse is needed. -- If trimming is needed, prefer range slicing over copying. -- If internal whitespace normalization is needed, use a reusable scratch buffer - owned by the provider or layout context. - -Acceptance tests: - -- Existing whitespace tests stay green: - - `UIRichText.WhitespaceCollapseTest` - - `UIRichText.WhitespaceCollapseCodeTest` - - `UIRichText.WhitespaceCollapseBRTest` - - `UITextNode_Regression.WhitespaceCollapseDoesNotCreateSpuriousNodes` - -### 6. UI Text Measurement Without Per-Run `Text` Allocation - -The current path creates `Text` objects for inline runs and render spans. The UI -path should eventually avoid that for layout measurement. - -Incremental strategy: - -1. Keep existing `Text` drawing for final rendering. -2. Introduce a measurement helper that can compute: - - width, - - wraps, - - min intrinsic width, - - max intrinsic width, - - line height, - - baseline, - from `String::View + FontStyleConfig`. -3. Cache shaped/wrapped results by source text node generation, style generation, - max width, and shaper settings. -4. Only materialize `Text` or shaped draw payloads for final draw fragments. - -Do not start with a large text/shaper rewrite. First remove repeated allocation -from obvious layout-only paths. - -### 7. UI Fragment Mapping - -Once the UI provider exists, `BlockLayouter::positionRichTextChildren()` should -consume UI fragments directly. - -Current `BlockLayouter` groups `RichText::InlineFragment`s by source pointer. -The UI path can avoid the map when possible: - -- Fragments already contain typed `UISourceRef`. -- During fragment generation, append fragment pointers/ranges directly to a - per-node layout cache. -- Each `UITextNode` / `UITextSpan` can receive hit boxes from its source - fragments without a separate `UnorderedMap` pass. - -Potential structures: - -```cpp -struct UIInlineLayoutResult { - SmallVector lines; - SmallVector fragments; - UnorderedMap sourceRanges; // transitional only -}; -``` - -Longer term, avoid `sourceRanges` by storing fragment ranges on the relevant UI -nodes during layout, or by preserving fragment order and resolving during the -same traversal. - -### 8. Drawing - -The UI-specific draw path should draw from UI fragments and source widgets. - -Rules: - -- Text fragments draw text using source text/style data. -- Inline box backgrounds should use the source widget background drawable when - it has real background data or radius. -- Font background color and widget background radius must compose correctly. -- Border drawables should come from the source widget. -- Split inline boxes must respect `startsInlineBox` and `endsInlineBox`. -- If the current drawable APIs cannot suppress continuation sides cleanly, add a - drawable-level side mask/clipping API rather than duplicating border painting - logic in RichText. - -Suggested follow-up API: - -```cpp -enum class BoxSideMask : Uint8 { - None = 0, - Left = 1 << 0, - Top = 1 << 1, - Right = 1 << 2, - Bottom = 1 << 3, - All = Left | Top | Right | Bottom -}; - -struct DrawableBoxPaintOptions { - BoxSideMask sides{ BoxSideMask::All }; - const Color* colorOverride{ nullptr }; -}; -``` - -Then add an overload for UI drawables that need it: - -```cpp -void draw( const Vector2f& position, const Sizef& size, - const DrawableBoxPaintOptions& options ); -``` - -Do not add this until the current fragment behavior is covered by tests. - -## Current RichText Optimization Plan - -These optimizations can be done even before the UI-specialized backend exists. - -### A. Reuse Persistent Output Storage - -Current public/internal storage: - -- `std::vector mInlineItems` -- `std::vector mInlineFragments` -- `std::vector mLines` -- `std::vector RenderParagraph::spans` -- `std::vector InlineItem::Box::children` - -Do not blindly replace all of these with `SmallVector`. - -Recommended changes: - -- Keep top-level `mInlineItems`, `mInlineFragments`, and `mLines` as - `std::vector` unless profiling shows most documents are tiny. These can grow - large in HTML. -- Preserve capacity across rebuilds. Prefer `clear()` and refill over assigning - a temporary vector. -- Change `RichTextInlineLayouter::rebuildFragments()` to fill an output vector - passed by reference: - -```cpp -static void rebuildFragments( const InlineItems& items, - const Lines& lines, - std::vector& out ); -``` - -- Avoid `mInlineFragments = rebuildFragments(...)` because it may discard useful - capacity or force extra moves. -- Consider `SmallVector` for `RenderParagraph::spans` only after - checking object size and typical spans-per-line. - -### B. Reduce `Text` Object Allocation - -High-priority allocation source: - -- `RichText::addInlineText()` allocates `std::shared_ptr`. -- `appendTextRenderSpan()` creates render text payloads for substrings. - -Incremental plan: - -1. Add a `RenderTextRun` payload that can reference: - - source `Text*`, or - - source string view/range plus `FontStyleConfig`. -2. Keep existing `Text` draw path initially. -3. Reuse `Text` objects from a per-`RichText` pool for render spans. -4. Reset and refill pooled `Text` objects during layout. -5. Later, replace pooled `Text` objects with a lighter shaped-text fragment if - Text supports enough low-level drawing hooks. - -Acceptance criteria: - -- `RichText.RichTextTest` remains green with shaper disabled/enabled/enabled - without optimizations. -- `FontRendering.TextBackgroundColor` remains green. -- Selection color application still works. - -### C. Cache Ancestor Metadata - -Current hot pattern: - -- paths are stored as `SmallVector`, -- helpers repeatedly resolve inline ancestor boxes, -- first/last leaf checks can recursively scan children. - -Optimization: - -- During layout-run construction, compute an `InlineAncestorChain` once. -- Store cached values on `InlineLayoutRun`: - - effective baseline alignment, - - inherited text decoration, - - start spacing, - - end spacing, - - first/last leaf flags for each ancestor edge, - - line-height edge contribution. - -This should reduce repeated calls to: - -- `resolveInlineBox()` -- `inlineAncestorStartSpacing()` -- `inlineAncestorEndSpacing()` -- `inlineAncestorTextDecoration()` -- `isFirstInlineLeafInBox()` -- `isLastInlineLeafInBox()` - -### D. Flatten Internal Inline Storage - -For generic `Graphics::RichText`, consider replacing nested child vectors with a -flat arena. - -Current: - -```cpp -struct Box { - std::vector children; -}; -``` - -Potential: - -```cpp -struct InlineItem { - InlineItemKind kind; - size_t firstChild; - size_t childCount; - size_t parent; - Payload payload; -}; - -std::vector mInlineItems; -``` - -Benefits: - -- fewer per-box heap allocations, -- better traversal locality, -- parent access without path resolution, -- child ranges instead of recursive vector ownership, -- easier fragment source indexing. - -Costs: - -- builder API becomes more complex, -- moving/erasing items is harder, -- tests and helper functions need significant updates. - -Recommendation: - -- Do not start here. -- First extract provider/layout seams. -- If we keep optimizing generic `RichText`, flat arena is the bigger structural - optimization after storage reuse and text allocation reduction. - -### E. Avoid Hot-Path Hash Maps Where Possible - -Current state: - -- `BlockLayouter` owns reusable `UnorderedMap` maps. -- Bucket lists use `SmallVector`. - -Next improvements: - -- For the UI-specialized backend, avoid grouping by pointer after layout. -- Emit source fragment ranges during fragment generation. -- If maps remain needed, consider: - - keeping them persistent and capacity-stable, - - using typed keys (`Node*`) instead of `void*`, - - clearing buckets without freeing bucket vectors, - - avoiding `operator[]` when lookup-only behavior is intended. - -## Migration Phases - -Every phase has a hard completion gate: - -- Build and all required focused tests for that phase must pass. -- The full unit test suite must pass unless the phase explicitly says it is - documentation-only and has no code changes. -- `git diff --check` must pass. -- A recovery checkpoint must be saved with `git stash`. -- The checkpoint must be re-applied immediately so the working tree keeps the - completed phase changes. - -Checkpoint workflow: - -```bash -git stash push -u -m "ui-inline-formatting-context phase N: " -git stash apply stash@{0} -``` - -Do not use `git stash pop` for checkpoints. The stash entry must remain in the -stash list as the last known-good recoverable snapshot. If a later phase fails or -becomes hard to unwind, recover from the latest passing phase checkpoint instead -of manually reconstructing the working tree. - -Each phase's final notes must record: - -- validation commands run, -- pass/fail counts, -- any known flaky reruns, especially Xvfb cookie failures, -- stash checkpoint message, -- stash reference if available. - -### Phase 0: Baseline And Metrics - -Purpose: establish behavior and performance numbers before refactoring. - -Tasks: - -- Record current focused test timings: - - `UIRichText.InvalidWidthLengthComputation3` - - `UIRichText.*` - - `UIBackground.*` - - `UIHTMLTable.complexLayout*` - - `UIHTMLFloat.*` -- Add a debug allocation counter or targeted instrumentation if available. -- Document the number of: - - inline items, - - render spans, - - inline fragments, - - text objects, - for representative HTML examples. - -Representative examples: - -- `bin/unit_tests/assets/html/background_positioning.html` -- inline anchor wrapping case, -- inline-block browser test, -- details/summary fixture, -- table complex layout fixture. - -Exit criteria: - -- Baseline numbers are recorded in the agent loop or this plan. -- No code behavior changes. -- Required validation passes. -- Stash checkpoint is created and re-applied, even if the phase only records - baseline documentation. This preserves a named restore point before code - refactoring starts. - -### Phase 1: Extract Shared Layout Run Provider - -Purpose: introduce the provider seam while preserving current behavior. - -Tasks: - -- Define provider/item-view types in a Graphics-safe location. -- Implement `GraphicsRichTextInlineProvider`. -- Convert `buildLayoutRuns()` to consume the provider. -- Keep downstream layout code unchanged as much as possible. - -Validation: - -- `RichText.*` -- `UIRichText.*` -- `UIHTMLFloat.*` -- `UIHTMLTable.complexLayout*` -- full suite -- `git diff --check` - -Exit criteria: - -- Output behavior is unchanged. -- Current RichText backend remains default. -- Required validation passes. -- Stash checkpoint is created and re-applied. - -### Phase 2: Extract Fragment Builder Provider Hooks - -Purpose: make fragment reconstruction independent of `RichText::InlineItem`. - -Tasks: - -- Move leaf collection and box accumulator logic behind provider/source queries. -- Keep `RichText::InlineFragment` output for the Graphics backend. -- Introduce parallel UI fragment types only if needed. - -Validation: - -- Selection rect tests. -- Hit testing tests. -- Wrapped inline border/background tests. -- Text decoration propagation tests. - -Exit criteria: - -- RichText backend still behaves exactly as before. -- Fragment construction no longer requires direct recursive access to - `std::vector`. -- Required validation passes. -- Stash checkpoint is created and re-applied. - -### Phase 3: Build Experimental UI Provider - -Purpose: walk the UI node tree directly. - -Tasks: - -- Add `UIInlineProvider`. -- Implement traversal for: - - direct text nodes, - - nested inline spans, - - inline-block widgets, - - floats, - - `
`, - - invisible and out-of-flow nodes. -- Port whitespace collapsing into provider state. -- Add a switch to run one `UIRichText` instance through the experimental backend. - -Validation: - -- Compare layout results between RichText backend and UI backend for selected - tests. -- Initially compare: - - total character count, - - selection string, - - line count, - - text node bounds, - - widget hit boxes, - - inline fragment bounds. - -Exit criteria: - -- Experimental backend can pass a small focused subset without becoming default. -- Existing default backend remains green. -- Required validation passes. -- Stash checkpoint is created and re-applied. - -### Phase 4: UI Layout Result And Mapping - -Purpose: remove the map-back step for the UI backend. - -Tasks: - -- Add `UIInlineLayoutResult`. -- Store source refs as typed `Node*` / `UIWidget*` / `UITextNode*`. -- Map text node bounds and widget hit boxes directly from source fragments. -- Keep `BlockLayouter` support for both current RichText fragments and new UI - fragments during transition. - -Validation: - -- `UITextNode_BlockLayouter.*` -- `UITextNode_RichTextRebuild.*` -- `UIRichText.selection` -- nested span over-find tests. - -Exit criteria: - -- UI backend does not need `RichText::InlineSource` or pointer grouping maps for - normal fragment mapping. -- Existing default backend remains green. -- Required validation passes. -- Stash checkpoint is created and re-applied. - -### Phase 5: UI Drawing Backend - -Purpose: draw inline UI fragments from source UI nodes and drawables. - -Tasks: - -- Draw inline backgrounds/borders from source widget drawables. -- Preserve font background color plus widget radius composition. -- Preserve split inline first/last edge flags. -- Keep text drawing consistent with current `RichText` behavior. -- Decide whether to add drawable side-mask API for perfect split border - continuation behavior. - -Validation: - -- `UIRichText.InlineParentFontBackgroundColorUsesBorderRadiusDrawable` -- `UIRichText.InlineParentBorderIsPreservedInFragments` -- `UIBackground.*` -- `UIBorder.*` -- image comparison tests if available. - -Exit criteria: - -- UI backend draws the same or better than current backend for covered cases. -- Existing default backend remains green. -- Required validation passes. -- Stash checkpoint is created and re-applied. - -### Phase 6: Performance And Memory Cleanup - -Purpose: remove the duplicated RichText inline tree from the UI backend. - -Tasks: - -- Stop calling `UIRichText::rebuildRichText()` for containers using UI backend. -- Keep generic `RichText` only as the public API for non-UI use. -- Remove transitional bridge fields from UI fragments when no longer needed. -- Keep current RichText bridge fields if generic RichText still needs them. -- Re-measure allocations and timings from Phase 0. - -Exit criteria: - -- UI backend is measurably better on memory allocations. -- No significant timing regression. -- Existing full suite passes. -- Required validation passes. -- Stash checkpoint is created and re-applied. - -### Phase 7: Make UI Backend Default - -Purpose: switch the production UI/HTML path. - -Tasks: - -- Flip default backend for `UIRichText`. -- Keep fallback switch temporarily for debugging. -- Update plans and comments that describe RichText reconstruction as the main UI - path. -- Remove fallback only after enough soak time. - -Validation: - -- Full suite. -- Targeted HTML/UI visual smoke tests. -- Any available screenshot/image-diff tests. - -Exit criteria: - -- UI backend is default. -- Generic `Graphics::RichText` remains green independently. -- Required validation passes. -- Stash checkpoint is created and re-applied. - -## Test Matrix - -Always run after broad changes: - -```bash -make -C make/linux -j$(nproc) -projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="RichText.*" -projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIRichText.*" -projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UITextNode_RichTextRebuild.*" -projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UITextNode_BlockLayouter.*" -projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTMLFloat.*" -projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTMLTable.complexLayout*" -projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIBackground.*" -projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug -git diff --check -``` - -If UI tests fail with: - -```text -Invalid MIT-MAGIC-COOKIE-1 key -``` - -rerun the failed filter sequentially before investigating unrelated crashes. - -## Checkpoint Policy - -Each implementation phase must leave a recoverable stash snapshot. The checkpoint -is not a replacement for keeping the working tree active; it is a safety net. - -Required sequence at the end of every passing phase: - -1. Run the phase-specific focused tests. -2. Run the full suite unless the phase had no code changes. -3. Run `git diff --check`. -4. Create a checkpoint: - -```bash -git stash push -u -m "ui-inline-formatting-context phase N: " -``` - -5. Re-apply it immediately: - -```bash -git stash apply stash@{0} -``` - -6. Confirm the working tree still contains the phase changes. -7. Record the stash message in `.agent/plans/first_class_inline_boxes_agent_loop.md` - or a dedicated continuation log for this plan. - -Important details: - -- Use `git stash push -u` so new plan files, new tests, fixtures, and other - untracked artifacts are included in the checkpoint. -- Do not use `git stash pop`; popping destroys the checkpoint. -- Do not checkpoint a failing phase as if it were passing. -- If a phase needs partial experimental work that does not pass yet, stash it - separately with a clear `wip-failing` message before reverting or switching - direction. Do not call that stash a phase checkpoint. -- If the worktree already contains unrelated user changes, do not revert them. - Include them in the checkpoint only if they are part of the active phase or - unavoidable in the shared working tree; otherwise document the dirty files - before checkpointing. - -## Acceptance Criteria - -The plan is complete when: - -- `Graphics::RichText` remains generic and independent of UI. -- UI/HTML inline layout can run without constructing a full - `RichText::InlineItem` mirror tree. -- Existing UI nodes remain the source of truth for widget style/drawable data. -- The UI backend preserves: - - drawing, - - background color, - - border radius, - - border drawing, - - background images, - - text decoration, - - selection string, - - selection rectangles, - - hit testing, - - text node bounds, - - inline widget hit boxes, - - float layout, - - inline-block layout, - - baseline and vertical-align behavior. -- Allocation counts and/or memory usage improve on representative HTML. -- Full unit suite passes. - -## Risks - -- Provider abstraction may become too generic and obscure the layout algorithm. - Keep item views concrete and small. -- UI provider traversal may accidentally diverge from current whitespace - behavior. Add tests before switching defaults. -- Text measurement without `Text` objects can drift from actual draw behavior. - Keep Text-backed rendering until measurement parity is proven. -- Fragment output can become fragmented across too many types. Avoid splitting - UI and Graphics outputs until a concrete dependency forces it. -- Drawable side masking may require deeper changes in border/background drawing. - Treat that as a follow-up fidelity feature unless tests require it. - -## Recommended First Task - -Start with Phase 0 and Phase 1 only. - -Do not begin by writing the UI provider. First extract a provider seam from the -current `RichText::InlineItem` path and prove that the generic backend still -passes all tests. That gives us a stable extension point for the UI provider -without risking a broad rewrite. diff --git a/.agent/plans/white_space_collapse_support_plan.md b/.agent/plans/white_space_collapse_support_plan.md deleted file mode 100644 index b54d831a1..000000000 --- a/.agent/plans/white_space_collapse_support_plan.md +++ /dev/null @@ -1,397 +0,0 @@ -# CSS `white-space-collapse` Support Plan - -## Goal - -Implement real CSS Text Level 4 `white-space-collapse` behavior in the HTML/RichText pipeline. - -The immediate product goal is that `
` can render as normal HTML/RichText content, preserving
-code indentation, blank lines, and line breaks without the current `UICodeEditor` fallback in
-`UIRichText::loadFromXmlNode()`.
-
-Reference specs:
-
-- CSS Text Module Level 4: https://drafts.csswg.org/css-text-4/#white-space-collapsing
-- Stable TR snapshot: https://www.w3.org/TR/css-text-4/#white-space-collapsing
-
-## Implementation Status
-
-Recovered and implemented in the current workspace on 2026-06-01, without refreshing any golden
-images.
-
-Implemented:
-
-- Raw PCDATA is preserved when loading `UIRichText` and `UITextSpan`; CSS whitespace processing now
-  happens during RichText rebuild.
-- `WhiteSpaceCollapse::Discard` exists and `white-space-collapse` parsing/stringification covers
-  `collapse`, `preserve`, `preserve-breaks`, `preserve-spaces`, `break-spaces`, and `discard`.
-- `white-space` accepts the legacy values plus direct collapse keywords and `wrap`/`nowrap` tokens.
-- `UIRichText::rebuildRichText()` applies the effective collapse mode for text nodes, inline spans,
-  and rebuilt `UITextSpan` roots.
-- `RichText` has a whitespace wrap mode for preserved and `break-spaces` content; `break-spaces`
-  contributes preserved spaces/tabs to intrinsic width and wraps after preserved spaces/tabs.
-- CSS `tab-size` is registered as inherited with initial value `8`; `UIRichText` stores the value and
-  passes it to `RichText`/`Text` measurement and wrapping.
-- Base HTML CSS now includes `pre { white-space: pre; }`.
-- Generic HTML `
` renders as normal RichText content.
-- The old read-only `UICodeEditor` initialization is retained only when a `
` block is
-  inside `UI_TYPE_MARKDOWNVIEW`, or when `UIRichText::setUseCodeEditorForPreCodeBlocks( true )` is
-  explicitly enabled. The global switch defaults to disabled.
-- Regression fixtures were restored:
-  - `bin/unit_tests/assets/html/pre.code.html`
-  - `bin/unit_tests/assets/html/pre.code.2.html`
-  - `bin/unit_tests/assets/html/pre_code_block.html`
-- A final segment break immediately before an element end tag is discarded before CSS whitespace
-  collapse/transformation, matching browser behavior for preformatted HTML such as
-  `
...\n
`. - -Validated: - -- `make -C make/linux -j$(nproc)` -- `UIHTML.PreCode*` -- `UIHTML.WhiteSpaceCollapse*` -- `UIRichText.WhiteSpaceCollapse*` -- `RichText.BreakSpaces*` -- `UITextNode_EdgeCases.EmptyTextNodesDontAffectLayout` -- `UIHTML.WhiteSpaceNowrap*` -- `UIHTML.AnchorsSizing` -- `UIHTMLTable.complexLayout*` against the original existing golden images -- Full Xvfb/ASAN unit suite: - `ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug` - ran 478 test cases: 477 passed, 1 skipped (`UIHTML.redditOldThreadWebViewSmoke`). -- `git diff --check` -- No golden images were refreshed for this feature. - -Pending implementation work: - -- None for the `white-space-collapse` support described by this plan. - -Known follow-up, not part of this recovery: - -- Browser-parity investigation for apparent high pixel-density `em`/font-size differences in code - blocks. At pixel density 1, the current `pre_code_block.html` spacing is covered by unit tests. - -## Baseline State Before This Plan - -- `UIRichText` already stores: - - `WhiteSpaceCollapse::Collapse` - - `WhiteSpaceCollapse::Preserve` - - `WhiteSpaceCollapse::PreserveBreaks` - - `WhiteSpaceCollapse::PreserveSpaces` - - `WhiteSpaceCollapse::BreakSpaces` -- CSS parsing already recognized `white-space` and `white-space-collapse`. -- `white-space: nowrap` is partially working because it maps to `mLineWrap = false`. -- The actual text processing was incomplete: - - `UIRichText::loadFromXmlNode()` collapses all PCDATA through - `UIRichText::collapseInternalWhitespace()` before style resolution can know the effective - `white-space-collapse` value. - - `UIRichText::rebuildRichText()` only distinguishes `Collapse` from all other values. - - `preserve-breaks`, `preserve-spaces`, and `break-spaces` do not get their spec behavior. - - `break-spaces` has no special line-breaking or intrinsic-size support. -- `
` was special-cased in `UIRichText::loadFromXmlNode()` into a read-only `UICodeEditor`.
-  That workaround is now limited to markdown/global opt-in only.
-
-## Spec Behavior To Support
-
-Implement the full `white-space-collapse` value set:
-
-| Value | Required behavior |
-|---|---|
-| `collapse` | Collapse white-space runs and segment breaks into a single space, with existing block-boundary trimming. |
-| `preserve` | Preserve spaces, tabs, and segment breaks. Segment breaks become forced line breaks. |
-| `preserve-breaks` | Collapse spaces/tabs, but preserve segment breaks as forced line breaks. |
-| `preserve-spaces` | Preserve spaces; convert tabs and segment breaks to spaces. |
-| `break-spaces` | Preserve spaces/tabs/segment breaks like `preserve`, but allow a wrap opportunity after every preserved space and make trailing preserved spaces measurable/non-hanging. |
-| `discard` | Spec-defined but not broadly implemented by browsers. Add enum/parser support and implement as "remove all collapsible white-space characters" so the engine is complete and deterministic. |
-
-`white-space` shorthand must continue mapping correctly:
-
-| `white-space` | collapse mode | wrap mode |
-|---|---|---|
-| `normal` | `collapse` | wrap |
-| `nowrap` | `collapse` | nowrap |
-| `pre` | `preserve` | nowrap |
-| `pre-wrap` | `preserve` | wrap |
-| `pre-line` | `preserve-breaks` | wrap |
-| `break-spaces` | `break-spaces` | wrap |
-
-Also accept two-keyword Level 4 forms where practical, e.g. `white-space: preserve nowrap`,
-`white-space: preserve wrap`, `white-space: collapse nowrap`, and direct
-`white-space-collapse: preserve-spaces`.
-
-## Implementation Plan
-
-### Phase 1: Preserve Source Text Until Layout
-
-**Files:**
-
-- `src/eepp/ui/uirichtext.cpp`
-- `include/eepp/ui/uirichtext.hpp`
-- `src/tests/unit_tests/richtext_tests.cpp`
-- `src/tests/unit_tests/uihtml_tests.cpp`
-
-Steps:
-
-1. Stop calling `collapseInternalWhitespace()` when creating `UITextNode` from PCDATA in
-   `UIRichText::loadFromXmlNode()`.
-2. Store raw parsed text in `UITextNode`; CSS whitespace processing must happen only during rich-text
-   rebuild/layout.
-3. Keep `collapseInternalWhitespace()` temporarily for existing callers/tests, but move toward a new
-   policy-driven helper.
-4. Add a focused regression proving that a raw text node under `white-space-collapse: preserve`
-   keeps repeated spaces and `\n` characters through `rebuildRichText()`.
-
-This phase must keep existing collapsed HTML indentation behavior by moving that collapse decision
-from load time to `rebuildRichText()`, not by preserving all source indentation in default layout.
-
-### Phase 2: Add A Whitespace Normalization Policy
-
-**Files:**
-
-- `include/eepp/ui/uirichtext.hpp`
-- `src/eepp/ui/uirichtext.cpp`
-
-Introduce a small helper around the current enum:
-
-```cpp
-struct WhiteSpaceProcessing {
-    WhiteSpaceCollapse collapse;
-    bool lineWrap;
-    bool preservesSegmentBreaks;
-    bool preservesSpaces;
-    bool convertsTabsToSpaces;
-    bool breakAfterSpaces;
-    bool discardWhitespace;
-};
-```
-
-The exact shape can be simplified, but the call site needs explicit booleans instead of repeated
-`collapse == X` checks.
-
-Add a helper with one responsibility:
-
-```cpp
-static String processWhiteSpaceForLayout(
-    String::View raw,
-    WhiteSpaceProcessing policy,
-    WhiteSpaceBoundary boundary );
-```
-
-`WhiteSpaceBoundary` should carry only the cross-node state needed by inline formatting:
-
-- whether the previous emitted text ended with collapsible space,
-- whether the logical previous/next item is inline,
-- whether block-boundary trimming is allowed,
-- whether the caller is inside an inline box.
-
-Keep this allocation-conscious:
-
-- Continue using `String::View` where text is passed through unchanged.
-- Allocate a transformed `String` only when the policy actually changes the bytes.
-- Reuse the existing local transformed string pattern already used for `text-transform`.
-
-### Phase 3: Apply Policy In `rebuildRichText()`
-
-**File:** `src/eepp/ui/uirichtext.cpp`
-
-Replace the current `shouldCollapse` boolean with a full effective policy:
-
-1. Resolve the effective whitespace mode from the nearest `UIRichText`/`UITextSpan` ancestor.
-2. Apply the same policy to:
-   - standalone `UITextNode` content,
-   - inline `UITextSpan::getText()` content,
-   - `UIRichText` self text when a `UITextSpan` is being rebuilt directly.
-3. Preserve the existing logical-prev/logical-next handling for default collapsed HTML whitespace.
-4. Make block-boundary trimming conditional:
-   - allowed for `collapse` and `preserve-breaks` around collapsible spaces,
-   - not allowed for `preserve`, `preserve-spaces`, or `break-spaces`.
-5. Treat `
` as a forced line break independent of the whitespace collapse mode. -6. Make `UITextNode::setLayoutCharCount()` reflect the processed layout string length, while leaving - raw node text intact. - -Expected examples: - -- Default HTML source indentation around block children still disappears. -- `white-space-collapse: preserve` keeps `" a\n b"`. -- `white-space-collapse: preserve-breaks` turns `" a\n b"` into `" a\n b"` behavior. -- `white-space-collapse: preserve-spaces` turns tabs/newlines into spaces without collapsing spaces. -- `white-space-collapse: discard` emits no spaces/tabs/newlines. - -### Phase 4: Teach `RichText` About `break-spaces` - -**Files:** - -- `include/eepp/graphics/richtext.hpp` -- `src/eepp/graphics/richtext.cpp` -- `include/eepp/graphics/linewrap.hpp` -- `src/eepp/graphics/linewrap.cpp` -- `src/tests/unit_tests/richtext_tests.cpp` - -`break-spaces` cannot be fully implemented only by preprocessing strings. It changes wrapping and -intrinsic sizing: - -1. Add a compact wrap behavior flag to `RichText`, for example: - - ```cpp - enum class WhiteSpaceWrapMode { - Normal, - Preserve, - BreakSpaces - }; - ``` - - Or use smaller booleans if that fits the existing code better. - -2. Set it from `UIRichText::rebuildRichText()` based on the effective container policy. -3. Thread the behavior into `RichTextInlineLayouter` and `LineWrap::computeLineBreaksEx()`. -4. For `break-spaces`, add a soft wrap opportunity after every preserved space/tab. -5. Ensure trailing preserved spaces contribute to rendered width and min/max intrinsic widths. -6. Keep the default `RichText` behavior unchanged for non-HTML callers. - -This is the phase most likely to expose assumptions in line wrapping and intrinsic sizing. Keep the -first patch narrow: only add the flags needed by `break-spaces`, then expand tests. - -### Phase 5: CSS Shorthand And Enum Completeness - -**Files:** - -- `include/eepp/ui/uirichtext.hpp` -- `src/eepp/ui/uirichtext.cpp` -- `src/eepp/ui/css/stylesheetspecification.cpp` -- `src/tests/unit_tests/uihtml_tests.cpp` - -Steps: - -1. Add `WhiteSpaceCollapse::Discard`. -2. Fix redundant checks in `toWhiteSpaceCollapse()` and include `discard`. -3. Extend `fromWhiteSpaceCollapse()` and `fromWhiteSpace()`. -4. Extend `applyWhiteSpace()` to parse: - - legacy single-keyword values, - - `break-spaces`, - - direct collapse keywords, - - `wrap` / `nowrap` when present as Level 4 shorthand components. -5. Keep `white-space-trim` out of scope unless it already exists elsewhere; explicitly document this - limitation near `applyWhiteSpace()` or in `html-layout-architecture.md`. - -### Phase 6: Replace The Generic `
` Fallback
-
-**File:** `src/eepp/ui/uirichtext.cpp`
-
-Once the whitespace behavior is covered:
-
-1. Remove the generic `mTag == "pre" && child.name() == "code"` branch that creates `UICodeEditor`.
-2. Keep the same `UICodeEditor` path only for `UI_TYPE_MARKDOWNVIEW` ancestors and for the explicit
-   global opt-in switch.
-3. Let ordinary HTML `` be created through `UIWidgetCreator::createFromName()` or the existing
-   fallback to `UITextSpan`.
-4. Ensure base HTML CSS gives the right defaults:
-   - `pre { white-space: pre; }` or equivalent.
-   - `code { font-family: monospace; }` if not already present.
-   - `pre code` remains normal inline/flow content inside the `
`.
-5. Preserve `data-language` as ordinary data/style metadata. Do not reintroduce syntax-highlighting
-   behavior through the generic HTML path in this feature.
-
-### Phase 7: Tests
-
-Add tests before removing the fallback.
-
-#### RichText Unit Tests
-
-Add focused tests in `src/tests/unit_tests/richtext_tests.cpp`:
-
-- `WhiteSpaceCollapseCollapseDefault`
-- `WhiteSpaceCollapsePreserveSpacesAndBreaks`
-- `WhiteSpaceCollapsePreserveBreaks`
-- `WhiteSpaceCollapsePreserveSpaces`
-- `WhiteSpaceCollapseBreakSpacesWrapsAfterSpaces`
-- `WhiteSpaceCollapseDiscard`
-- `WhiteSpaceCollapseAcrossInlineSpanBoundaries`
-
-Assertions should inspect generated lines, text fragments, or text-node layout char counts. Avoid
-pixel-only assertions unless testing trailing-space width.
-
-#### UIHTML Tests
-
-Add focused tests in `src/tests/unit_tests/uihtml_tests.cpp`:
-
-- `WhiteSpaceCollapsePreCodePreservesIndentation`
-- `PreCodeUsesCodeEditorOnlyForMarkdownAncestorOrGlobalOptIn`
-- `WhiteSpaceCollapsePreLinePreservesBreaksOnly`
-- `WhiteSpaceCollapseBreakSpacesAffectsIntrinsicWidth`
-
-For the `
` regression:
-
-```html
-
if (x) {
-    return 1;
-}
-
-``` - -Verify: - -- generic HTML `code` is not a `UICodeEditor`. -- markdown/global opt-in `code` is a `UICodeEditor`. -- indentation before `return` survives. -- line count is at least 3. -- the `
`/`` content participates in normal RichText layout.
-
-#### Existing Tests To Re-run
-
-Focused:
-
-```sh
-projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIRichText.*WhiteSpace*"
-projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.WhiteSpace*"
-projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UITextNode.*"
-projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTMLFloat.*whitespace*"
-```
-
-Broader:
-
-```sh
-make -C make/linux -j$(nproc)
-projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.*"
-projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug
-git diff --check
-```
-
-If the local runner uses the older direct `xvfb-run` form, keep the same ASAN/screen settings:
-
-```sh
-ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.WhiteSpace*"
-```
-
-## Risks And Constraints
-
-- Do not collapse raw text at parse time. The spec says whitespace processing is for rendering and
-  must not mutate underlying document data.
-- Do not make a tag-specific `
` or `` workaround. The feature belongs to CSS text
-  processing and inline formatting.
-- Be careful with source indentation in fixture HTML. Removing parse-time collapse will expose many
-  indentation-only text nodes unless default `collapse` behavior remains correct in `rebuildRichText()`.
-- `break-spaces` affects the line breaker and intrinsic sizing. It is not enough to preserve bytes.
-- Keep changes allocation-conscious. The RichText rebuild path is hot during layout invalidation.
-- Preserve current inline metadata behavior: backgrounds, borders, selection, anchor hit boxes,
-  inline-block baselines, floats, and out-of-flow skipping.
-
-## Suggested Patch Order
-
-1. Add tests documenting current failure for preserved raw text and `
`.
-2. Move PCDATA collapse from load time to policy-driven rebuild time.
-3. Implement all non-`break-spaces` collapse modes.
-4. Add RichText/LineWrap support for `break-spaces`.
-5. Add `discard` parser/state behavior.
-6. Restrict the `
` `UICodeEditor` fallback to markdown/global opt-in.
-7. Update `html-layout-architecture.md` with the final whitespace-processing notes.
-8. Run focused tests, then `UIHTML.*`, then the full suite.
-
-## Completion Criteria
-
-- All `white-space-collapse` values have parser, property-string, layout, and test coverage.
-- Existing `white-space: nowrap` behavior remains passing.
-- Generic HTML `
` renders through normal HTML/RichText nodes, not `UICodeEditor`.
-- Markdown `
` can still render through read-only `UICodeEditor`.
-- Code indentation and blank lines are preserved in `
` and `white-space: pre` content.
-- Default HTML whitespace still collapses around block and inline boundaries.
-- `break-spaces` wraps and measures trailing spaces according to CSS Text Level 4.
-- Full unit suite passes under Xvfb/ASAN.
diff --git a/bin/unit_tests/assets/html/hn_empty_thread.html b/bin/unit_tests/assets/html/hn_empty_thread.html
new file mode 100644
index 000000000..0578e65fe
--- /dev/null
+++ b/bin/unit_tests/assets/html/hn_empty_thread.html
@@ -0,0 +1,283 @@
+
+  
+    
+    
+    
+    
+    
+    
+    
+      Show HN: Keybench – Scriptable, extensible performance tool for key value
+      stores | Hacker News
+    
+  
+  
+    
+ + + + + + + + + + + + + +
+ + + + + + + + +
+ + + Hacker Newsnew | + threads + | + past | + comments + | ask | + show | + jobs | + submit + + SpartanJ + (210) | + logout +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + + Show HN: Keybench – Scriptable, extensible + performance tool for key value stores + (github.com/guycipher) +
+ 7 points + by + alexpadula + 2 hours ago + | + flag + | + hide + | + past + | + favorite + | + discuss +
+
+ I've been working with storage engines for quite a while + and really there was no similar tool to sysbench and or + HammerDB for key-value storage engines. Thus I introduce + a POC called keybench. I hope you check it out, and do + give it a run to drive your favorite engine. +

Cheers!

+
+
+
+  help

+ +
+
+
+
+
+ + + + + + + +
+
+
+ Guidelines + | + FAQ | + Lists | + API | + Security + | Legal | + Apply to YC | + Contact

+
+ Search: + +
+
+
+
+ + + diff --git a/bin/unit_tests/assets/html/inline_block_wrap_defer.html b/bin/unit_tests/assets/html/inline_block_wrap_defer.html new file mode 100644 index 000000000..ec77f64d8 --- /dev/null +++ b/bin/unit_tests/assets/html/inline_block_wrap_defer.html @@ -0,0 +1,220 @@ + + + + + + + Space Cadet Pinball on Linux • Stephen Brennan + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +

Space Cadet Pinball on Linux

+Stephen Brennan • 09 May 2026 + +

To my fellow Linux users who grew up using Windows XP: did you know, you can +have Space Cadet Pinball on your Linux machine? This is not breaking news, but +it’s exciting to me, and I’m the one who decides what I write about. So here’s +your PSA!

+ +

Space Cadet Pinball was bundled with Windows XP, and growing up I played it a +lot. As a result it holds a special place in my heart. I found that it was the +most engaging game that was bundled with Windows. Solitaire was too mindless, +and Freecell, Hearts, and Minesweeper were too complex and boring to me at that +age. But pinball held my attention, and so I played it a lot1.

+ +

Anyway, somebody has gone to the effort of using a decompiler and reverse +engineering tools to create source code, and then put in what I’d imagine is a +lot of effort to make it playable on a lot of platforms! All that results in +this project on Github. The +easiest way to play on Linux is actually to use the Flatpak, which comes bundled +with the original game resources from the Windows version. You can either +install it with a GUI (e.g. KDE Discover) or install on the CLI:

+ +
flatpak install com.github.k4zmu2a.spacecadetpinball
+
+ +

That’s all you need to play & get a hit of nostalgia! I know there are +browser-based versions available too, but I would +rather have it installed on my computer directly.

+ +

High(er) Resolution With Full Tilt Data

+ +

Graphics technology has come a long way since then, and the graphics are a bit +rough at 480p. But fear not: another version of the game existed, called Full +Tilt! Pinball. Its game data is capable of displaying at the massive screen +resolution 1024x768! You can find this game data on +archive.org in a zip file.

+ +

Getting the flatpak version to use these data files is a bit tricky. The easiest +way is:

+ +
    +
  1. Ensure that you’ve run the game at least once prior to this, so that the data +directory is created.
  2. +
  3. Extract the downloaded zip file directly into your data directory: +
    cd ~/.var/app/com.github.k4zmu2a.spacecadetpinball/data/SpaceCadetPinball
    +unzip ~/Downloads/CADET.ZIP
    +
    +
  4. +
  5. Delete (or, if you’re cautious, rename) the old data directory which is +bundled with the app. Unfortunately this is necessary because the game +searches multiple locations for data, but once it finds data files in one +directory, it won’t continue looking for files in other locations. +
    sudo rm -r $(flatpak info --show-location com.github.k4zmu2a.spacecadetpinball)/files/extra/Pinball
    +
    +

    You may not need the sudo call if your installation was per-user. Mine got +installed to /var/lib/flatpak so I needed root.

    +
  6. +
+ +

It’s possible you’d need to repeat step 3 if the game gets updated. However, the +flatpak +hasn’t seen an update in over two years. I doubt one will happen, and if it +does, it won’t be frequent.

+ +

Random Notes

+ +
    +
  1. +

    If you want, you can keep the original files and merge them together so that +you have the full set of both the original (referred to as “3DPB” for 3D +Pinball in the game) and the new (Full Tilt) data. Then, the game will let +you toggle between them if you’d like.

    +
  2. +
  3. +

    The data files seem to have some impact on the game rules. For instance, in +the original 3DPB version, the reentry lanes (and launch lanes) have lights +which toggle as the ball passes over. In the Full Tilt version, the lights +stay on (rather than toggling), making it easier to complete the set of +lights and upgrade the associated set of bumpers. (Yes yes, I know I’m a nerd +for noticing this.)

    +
  4. +
  5. +

    Apparently, there was a game called “Marble Blast” which came pre-installed +on some Macs in a roughly similar time period, which I know people developed +similar relationships to growing up. Unlike this pinball game, the Marble +Blast series grew, and there are newer versions available to play today.

    +
  6. +
+ +

Bonus Thoughts

+ +

I think it’s great that this old game was beloved to enough people (and +especially at least one very competent & motivated person) to do this work. +Having (any) source code available makes this game portable to all sorts of +platforms, which is really great. You can play this on Mac, Windows, Linux, and +even Android & Nintendo Switch, apparently.

+ +

Personally, I would be happy to pay the original developers for their work on +this game, and I understand that there’s some concern about the legality of +downloading game data files, especially for the Full Tilt version of the game. +After all, they are copyrighted art & data which was part of a commercial +product. It seems like an unpopular opinion in today’s world, but I don’t +advocate for piracy. Paying people for their work is important, even when it +feels like you’re paying a faceless corporation. While I’d prefer things be +created with a FOSS license, the world doesn’t always work that way. At the end +of the day, I want people to be paid to create good things, because that’s how +we get more good things!

+ +

On the other hand, I feel software preservation is an important goal too. +Ideally, I’d like to see a world where proprietary software like this could be +placed into some sort of source code +escrow. As long as the +original copyright holders are in the business of selling their product, their +rights should be respected. But if they elect to stop selling it, I think that +code should revert to a FOSS license that allows users to improve & maintain the +software they use. This would help balance the rights of creators, users, and +the goals of preservation.

+ +
+ +
+
    +
  1. +

    I also played another pinball game called 3D Ultra Pinball, but that came + on a CD presumably for purchase. I have no idea whether my parents went + out and bought it, or if it came in a cereal box. (Yes, game CDs did show + up in cereal boxes sometimes.) ↩

    +
  2. +
+
+ + + +
+ + + +
+
+ Legal • + RSS +

+ Creative Commons License +

+ Stephen Brennan's Blog is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License +
+ +
+ + + diff --git a/bin/unit_tests/assets/html/inline_block_wrap_files/88x31.png b/bin/unit_tests/assets/html/inline_block_wrap_files/88x31.png new file mode 100644 index 000000000..41edcfd50 Binary files /dev/null and b/bin/unit_tests/assets/html/inline_block_wrap_files/88x31.png differ diff --git a/bin/unit_tests/assets/html/inline_block_wrap_files/main.css b/bin/unit_tests/assets/html/inline_block_wrap_files/main.css new file mode 100644 index 000000000..5e0066741 --- /dev/null +++ b/bin/unit_tests/assets/html/inline_block_wrap_files/main.css @@ -0,0 +1 @@ +div.site-content{margin-left:auto;margin-right:auto;margin-top:10px;padding-left:10px;padding-right:10px;overflow:hidden}@media (min-width: 560px) and (max-width: 799px){div.site-content{margin-left:5%;margin-right:5%}}@media (min-width: 800px){div.site-content{width:768px}}aside{display:block;float:left;width:50%;max-width:250px}@media (max-width: 560px){aside{width:100%;max-width:100%;clear:both;margin-left:auto;margin-right:auto}}body{font-family:sans-serif;color:#657B83;background-color:#FDF6E3;font-size:16px}code{font-size:14px}p,li{font-size:16px;line-height:1.5em}a:link,a:visited,a:hover,a:active{color:#B58900;text-decoration:none}a:hover{text-decoration:underline}img.body-responsive{max-width:100%;height:auto;margin-left:auto;margin-right:auto;display:block}img.latex-inline{max-height:0.85em}blockquote{background:#f9f9f9;border-left:10px solid #ccc;margin:1.5em 10px;padding:0.5em 10px;quotes:"“" "”" "‘" "’"}blockquote p{display:inline}table{border:collapse}th,td{padding:5px}table,th,td{border:1px solid #657B83}.social{text-align:center}.social li{display:inline-block;padding-left:20px}.clearfix{clear:both}nav{padding:15px}nav *{box-sizing:border-box}nav ul{float:left}nav ul li{display:inline-block;float:left}nav ul li:not(:first-child){margin-left:20px}nav ul li a{display:inline-block;outline:none;color:#1F2227;font-size:16px}nav ul li a:hover{color:gray}.title{font-size:1.6em}.title:hover{text-decoration:none}@media screen and (max-width: 560px){.nav-container{position:fixed;top:0;left:0;height:100%;width:100%;z-index:-1;background:#1F2227;opacity:0;transition:all .2s ease}.nav-container ul{position:absolute;top:50%;transform:translateY(-50%);width:100%}.nav-container ul li{display:block;float:none;width:100%;text-align:right;margin-bottom:10px}.nav-container ul li:nth-child(1) a{transition-delay:.2s}.nav-container ul li:nth-child(2) a{transition-delay:.3s}.nav-container ul li:nth-child(3) a{transition-delay:.4s}.nav-container ul li:nth-child(4) a{transition-delay:.5s}.nav-container ul li:not(:first-child){margin-left:0}.nav-container ul li a{padding:10px 25px;opacity:0;color:white;font-size:24px;font-weight:600;transform:translateY(-20px);transition:all .2s ease}.nav-open{position:fixed;right:10px;top:10px;display:block;width:48px;height:48px;cursor:pointer;z-index:9999;border-radius:50%}.nav-open i{display:block;width:20px;height:2px;background:#1F2227;border-radius:2px;margin-left:14px}.nav-open i:nth-child(1){margin-top:16px}.nav-open i:nth-child(2){margin-top:4px;opacity:1}.nav-open i:nth-child(3){margin-top:4px}}#nav:checked+.nav-open{transform:rotate(45deg)}#nav:checked+.nav-open i{background:white;transition:transform .2s ease}#nav:checked+.nav-open i:nth-child(1){transform:translateY(6px) rotate(180deg)}#nav:checked+.nav-open i:nth-child(2){opacity:0}#nav:checked+.nav-open i:nth-child(3){transform:translateY(-6px) rotate(90deg)}#nav:checked ~ .nav-container{z-index:9990;opacity:1}#nav:checked ~ .nav-container ul li a{opacity:1;transform:translateY(0)}.hidden{display:none}.main-list-item{font-weight:bold;margin:0.8em 0}.fa-ul.main-list{margin-left:0}.main-list-item-icon{width:36px;color:#657B83}.logo-container{text-align:center}.logo{margin:5px;width:96px;height:96px;border-radius:10%;display:inline-block;background-size:cover;border:2px solid #FDF6E3;box-shadow:0 0 0 3px #657B83}hr{border:0;height:1px;background-image:-webkit-linear-gradient(left, transparent, #657B83, transparent);background-image:-moz-linear-gradient(left, transparent, #657B83, transparent);background-image:-ms-linear-gradient(left, transparent, #657B83, transparent);background-image:-o-linear-gradient(left, transparent, #657B83, transparent)}footer{position:fixed;bottom:0;right:0;height:20px}.poweredby{font-family:"Arial Narrow", Arial;font-size:0.6em;line-height:0.6em;padding:0 5px;margin-top:30px;text-align:center}.highlight{background-color:#EEE8D5;padding:3px 3px 3px 3px;border-radius:7px;margin:10px 0 10px 0;overflow-x:auto}.highlight .c{color:#93A1A1}.highlight .err{color:#586E75}.highlight .g{color:#586E75}.highlight .k{color:#859900}.highlight .l{color:#586E75}.highlight .n{color:#586E75}.highlight .o{color:#859900}.highlight .x{color:#CB4B16}.highlight .p{color:#586E75}.highlight .cm{color:#93A1A1}.highlight .cp{color:#859900}.highlight .c1{color:#93A1A1}.highlight .cs{color:#859900}.highlight .gd{color:#2AA198}.highlight .ge{color:#586E75;font-style:italic}.highlight .gr{color:#DC322F}.highlight .gh{color:#CB4B16}.highlight .gi{color:#859900}.highlight .go{color:#586E75}.highlight .gp{color:#586E75}.highlight .gs{color:#586E75;font-weight:bold}.highlight .gu{color:#CB4B16}.highlight .gt{color:#586E75}.highlight .kc{color:#CB4B16}.highlight .kd{color:#268BD2}.highlight .kn{color:#859900}.highlight .kp{color:#859900}.highlight .kr{color:#268BD2}.highlight .kt{color:#DC322F}.highlight .ld{color:#586E75}.highlight .m{color:#2AA198}.highlight .s{color:#2AA198}.highlight .na{color:#586E75}.highlight .nb{color:#B58900}.highlight .nc{color:#268BD2}.highlight .no{color:#CB4B16}.highlight .nd{color:#268BD2}.highlight .ni{color:#CB4B16}.highlight .ne{color:#CB4B16}.highlight .nf{color:#268BD2}.highlight .nl{color:#586E75}.highlight .nn{color:#586E75}.highlight .nx{color:#555}.highlight .py{color:#586E75}.highlight .nt{color:#268BD2}.highlight .nv{color:#268BD2}.highlight .ow{color:#859900}.highlight .w{color:#586E75}.highlight .mf{color:#2AA198}.highlight .mh{color:#2AA198}.highlight .mi{color:#2AA198}.highlight .mo{color:#2AA198}.highlight .sb{color:#93A1A1}.highlight .sc{color:#2AA198}.highlight .sd{color:#586E75}.highlight .s2{color:#2AA198}.highlight .se{color:#CB4B16}.highlight .sh{color:#586E75}.highlight .si{color:#2AA198}.highlight .sx{color:#2AA198}.highlight .sr{color:#DC322F}.highlight .s1{color:#2AA198}.highlight .ss{color:#2AA198}.highlight .bp{color:#268BD2}.highlight .vc{color:#268BD2}.highlight .vg{color:#268BD2}.highlight .vi{color:#268BD2}.highlight .il{color:#2AA198} diff --git a/bin/unit_tests/assets/html/inline_block_wrap_files/normalize.css b/bin/unit_tests/assets/html/inline_block_wrap_files/normalize.css new file mode 100644 index 000000000..458eea1ea --- /dev/null +++ b/bin/unit_tests/assets/html/inline_block_wrap_files/normalize.css @@ -0,0 +1,427 @@ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * Improve readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/** + * Address styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove border when inside `a` element in IE 8/9/10. + */ + +img { + border: 0; +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index 01b3af5a4..97c979a49 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -963,8 +963,9 @@ class EE_API UISceneNode : public SceneNode { * (files, URLs, VFS). * * @param uri URI to load + * @param defer Defer some specific time the CSS load (0 to just load it asynchronously) */ - void loadCSS( URI uri ); + void loadCSS( URI uri, std::optional