29 KiB
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::RichTextremains a generic graphics primitive.- The current
RichText::InlineItempath 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:
UIRichText::rebuildRichText()walks the UI child tree.- It builds a parallel
Graphics::RichText::mInlineItemstree. RichTextInlineLayouterflattens that tree into layout runs.- Layout produces
RenderParagraph,RenderSpan, andInlineFragmentdata. BlockLayouter::positionRichTextChildren()groups fragments by source node and maps geometry back to UI widgets/text nodes.- Drawing uses
RenderSpanfor text/atomic payloads andInlineFragmentfor 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/UITextNodeRichText::InlineItemInlineLayoutRunRenderSpanInlineFragmentBlockLayouter::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::RichTextdepend on UI types. - Do not remove
Graphics::RichText::InlineItemin 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(), ormBlocks. - Do not add compatibility layers for deleted RichText block APIs.
Architectural Direction
Split the inline formatting engine into two conceptual layers:
- A storage-agnostic inline layout core.
- 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.
- Reads
UIInlineProvider- Walks
Node/UIWidget/UITextNodechildren directly. - Resolves CSS/UI metrics from existing widgets.
- Produces UI-oriented fragments that reference source nodes.
- Walks
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:
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:
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:
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:
struct InlineLineBox {
Float y;
Float height;
Float baseline;
Float width;
SmallVector<InlineFragmentRef, 16> 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:
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<RichText::InlineItem>.
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:
- Build layout runs from a provider.
- Keep the rest of
RichTextInlineLayouterunchanged. - 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
UIWidgetstate. - 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:
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:
struct InlineWhitespaceState {
bool shouldCollapse;
bool lastRunEndsWithSpace;
bool atBlockBoundary;
};
- For each text node, produce a
String::Viewor a compact transformed buffer. - Avoid allocating a new
Stringwhen 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.WhitespaceCollapseTestUIRichText.WhitespaceCollapseCodeTestUIRichText.WhitespaceCollapseBRTestUITextNode_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:
- Keep existing
Textdrawing for final rendering. - Introduce a measurement helper that can compute:
- width,
- wraps,
- min intrinsic width,
- max intrinsic width,
- line height,
- baseline,
from
String::View + FontStyleConfig.
- Cache shaped/wrapped results by source text node generation, style generation, max width, and shaper settings.
- Only materialize
Textor 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::InlineFragments 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/UITextSpancan receive hit boxes from its source fragments without a separateUnorderedMappass.
Potential structures:
struct UIInlineLayoutResult {
SmallVector<InlineLineBox, 8> lines;
SmallVector<UIInlineFragment, 64> fragments;
UnorderedMap<Node*, FragmentRange> 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
startsInlineBoxandendsInlineBox. - 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:
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:
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<InlineItem> mInlineItemsstd::vector<InlineFragment> mInlineFragmentsstd::vector<RenderParagraph> mLinesstd::vector<RenderSpan> RenderParagraph::spansstd::vector<InlineItem> InlineItem::Box::children
Do not blindly replace all of these with SmallVector.
Recommended changes:
- Keep top-level
mInlineItems,mInlineFragments, andmLinesasstd::vectorunless 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:
static void rebuildFragments( const InlineItems& items,
const Lines& lines,
std::vector<InlineFragment>& out );
- Avoid
mInlineFragments = rebuildFragments(...)because it may discard useful capacity or force extra moves. - Consider
SmallVector<RenderSpan, 8>forRenderParagraph::spansonly after checking object size and typical spans-per-line.
B. Reduce Text Object Allocation
High-priority allocation source:
RichText::addInlineText()allocatesstd::shared_ptr<Text>.appendTextRenderSpan()creates render text payloads for substrings.
Incremental plan:
- Add a
RenderTextRunpayload that can reference:- source
Text*, or - source string view/range plus
FontStyleConfig.
- source
- Keep existing
Textdraw path initially. - Reuse
Textobjects from a per-RichTextpool for render spans. - Reset and refill pooled
Textobjects during layout. - Later, replace pooled
Textobjects with a lighter shaped-text fragment if Text supports enough low-level drawing hooks.
Acceptance criteria:
RichText.RichTextTestremains green with shaper disabled/enabled/enabled without optimizations.FontRendering.TextBackgroundColorremains green.- Selection color application still works.
C. Cache Ancestor Metadata
Current hot pattern:
- paths are stored as
SmallVector<size_t, 4>, - helpers repeatedly resolve inline ancestor boxes,
- first/last leaf checks can recursively scan children.
Optimization:
- During layout-run construction, compute an
InlineAncestorChainonce. - 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:
struct Box {
std::vector<InlineItem> children;
};
Potential:
struct InlineItem {
InlineItemKind kind;
size_t firstChild;
size_t childCount;
size_t parent;
Payload payload;
};
std::vector<InlineItem> 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:
BlockLayouterowns reusableUnorderedMap<void*, FragmentBucket>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 ofvoid*, - 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 --checkmust 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:
git stash push -u -m "ui-inline-formatting-context phase N: <short description>"
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.InvalidWidthLengthComputation3UIRichText.*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::InlineFragmentoutput 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<RichText::InlineItem>. - 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,
<br>,- invisible and out-of-flow nodes.
- Port whitespace collapsing into provider state.
- Add a switch to run one
UIRichTextinstance 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
BlockLayoutersupport 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::InlineSourceor 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
RichTextbehavior. - Decide whether to add drawable side-mask API for perfect split border continuation behavior.
Validation:
UIRichText.InlineParentFontBackgroundColorUsesBorderRadiusDrawableUIRichText.InlineParentBorderIsPreservedInFragmentsUIBackground.*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
RichTextonly 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::RichTextremains green independently. - Required validation passes.
- Stash checkpoint is created and re-applied.
Test Matrix
Always run after broad changes:
make -C make/linux -j$(nproc)
ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug --filter="RichText.*"
ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug --filter="UIRichText.*"
ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug --filter="UITextNode_RichTextRebuild.*"
ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug --filter="UITextNode_BlockLayouter.*"
ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug --filter="UIHTMLFloat.*"
ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug --filter="UIHTMLTable.complexLayout*"
ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug --filter="UIBackground.*"
ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug
git diff --check
If UI tests fail with:
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:
- Run the phase-specific focused tests.
- Run the full suite unless the phase had no code changes.
- Run
git diff --check. - Create a checkpoint:
git stash push -u -m "ui-inline-formatting-context phase N: <short description>"
- Re-apply it immediately:
git stash apply stash@{0}
- Confirm the working tree still contains the phase changes.
- Record the stash message in
.agent/plans/first_class_inline_boxes_agent_loop.mdor a dedicated continuation log for this plan.
Important details:
- Use
git stash push -uso 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-failingmessage 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::RichTextremains generic and independent of UI.- UI/HTML inline layout can run without constructing a full
RichText::InlineItemmirror 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
Textobjects 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.