docs(ui): document presentation determinism
This commit is contained in:
parent
4f85a76033
commit
f9530a9164
1 changed files with 120 additions and 0 deletions
|
|
@ -1157,6 +1157,126 @@ The same interaction contract should apply to images, wiki links, embeds, Mermai
|
|||
|
||||
This means future rendered widgets can be added without changing the fundamental editor architecture.
|
||||
|
||||
## Finding #18 — Presentation Determinism
|
||||
|
||||
Milestone 3.4 moves determinism down from the semantic render model into the TextKit presentation layer. The render model was already stable after Milestone 3.3, but the visible editor could still diverge because focus state, stale paragraph attributes, and checklist placeholder geometry affected how the same model was materialized.
|
||||
|
||||
Presentation pipeline:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Model["DocumentRenderModel / DocumentPresentationState"] --> Styler["MarkdownTextStyler"]
|
||||
Styler --> Storage["NSTextStorage attributed runs"]
|
||||
Storage --> Layout["NSLayoutManager + NSTextContainer"]
|
||||
Layout --> Geometry["Line fragments, wrapping, baselines"]
|
||||
Model --> Widgets["Rendered element overlays"]
|
||||
Widgets --> Controls["Checklist NSButton controls"]
|
||||
Geometry --> View["NSTextView visual output"]
|
||||
Controls --> View
|
||||
```
|
||||
|
||||
Layer audit:
|
||||
|
||||
| Layer | Inputs | Outputs | Cached state | Invalidation |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `DocumentRenderModel` | Markdown source, line index | Semantic nodes and render snapshot | None | Recreated from document state |
|
||||
| `DocumentPresentationState` | Line index, effective active line | Source/rendered line states and semantic elements | None | Recreated for full or dirty line set |
|
||||
| `MarkdownTextStyler` | Presentation state, dirty plan, colors | `NSTextStorage` attributes | None | Full render or dirty lines |
|
||||
| TextKit layout | Attributed storage, container width | Glyphs, line fragments, wrapping | TextKit glyph/layout caches | Invalidated by attributed edits |
|
||||
| Checklist overlays | Rendered task elements, source ranges | Native checkbox frames | Existing buttons keyed by line index | Full rebuild or dirty task lines |
|
||||
|
||||
Root causes found:
|
||||
|
||||
- Initial heading source display was caused by treating the text view selection as active even when the editor had not been focused by the user. Opening a document selected offset 0, so the first heading was presented as source.
|
||||
- Paragraph reflow after interaction was caused by dirty styling resetting only `EditorLine.contentRange`. The newline character kept its previous paragraph style, so TextKit could lay out the same paragraph differently after active-line transitions.
|
||||
- Checklist spacing could shift because the checkbox source marker was fully collapsed to a 0.1 pt hidden font while a native overlay was positioned over that range. After interaction, overlay state and collapsed placeholder geometry were not a stable visual contract.
|
||||
|
||||
Corrections:
|
||||
|
||||
- The macOS and iOS adapters no longer auto-focus the editor on document open.
|
||||
- Presentation active line is now `-1` while the native text view is not first responder, so initial document load renders all lines.
|
||||
- Focus begin/end events explicitly restyle the presentation, making the source/rendered transition deterministic when the user enters or leaves the editor.
|
||||
- Dirty styling now resets and applies paragraph-level attributes over the full presentation paragraph range, including line endings.
|
||||
- Rendered checklist controls preserve layout by hiding checkbox source text with transparent text while retaining a fixed layout placeholder instead of collapsing it completely.
|
||||
|
||||
Presentation snapshot validation:
|
||||
|
||||
`MarkdownPresentationSnapshot` captures:
|
||||
|
||||
- attributed runs
|
||||
- font identity and point size
|
||||
- hidden syntax state
|
||||
- foreground/background alpha
|
||||
- paragraph style values
|
||||
- TextKit line fragment geometry for a fixed text container
|
||||
|
||||
Added tests:
|
||||
|
||||
- `testInitialUnfocusedPresentationRendersHeading`
|
||||
- `testDirtyActiveLineRoundTripMatchesFreshPresentation`
|
||||
- `testChecklistContentGeometryIsStableAcrossToggleState`
|
||||
- `testParagraphGeometryIsStableAcrossRepeatedPresentation`
|
||||
|
||||
These tests verify that unfocused initial presentation renders headings, dirty active-line transitions can return to the exact fresh presentation signature, checklist content origin does not shift between `[ ]` and `[x]`, and repeated paragraph presentation produces identical geometry.
|
||||
|
||||
Release benchmark after Milestone 3.4:
|
||||
|
||||
| Scenario | Total | Dirty typing render | Dirty lines |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| sample document | 11.544 ms | 0.189 ms | 3 |
|
||||
| 2,100-line prototype | 399.498 ms | 0.337 ms | 3 |
|
||||
| 5 MB benchmark | 3,034.613 ms | 0.863 ms | 3 |
|
||||
|
||||
The paragraph-range fix does not regress scalability. The 5 MB dirty typing path remains under 1 ms and still styles only three lines.
|
||||
|
||||
## Finding #19 — Render Model vs Presentation Layer
|
||||
|
||||
Milestone 3.4 clarifies the boundary between render determinism and presentation determinism.
|
||||
|
||||
The render model answers:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Markdown["Markdown source"] --> RenderModel["DocumentRenderModel"]
|
||||
RenderModel --> Snapshot["RenderSnapshot"]
|
||||
```
|
||||
|
||||
The presentation layer answers:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
RenderModel["Render model + active-line state"] --> Attributes["Attributed presentation"]
|
||||
Attributes --> TextKit["TextKit geometry"]
|
||||
TextKit --> Overlays["Rendered widget overlays"]
|
||||
Overlays --> Visual["Visual editor output"]
|
||||
```
|
||||
|
||||
Important distinction:
|
||||
|
||||
- `DocumentRenderModel` is independent of interaction state.
|
||||
- `DocumentPresentationState` intentionally includes the current active line when the editor is focused.
|
||||
- The native presentation must be deterministic for the same Markdown, same focus state, same active line, and same container width.
|
||||
|
||||
The active line is now an explicit presentation input, not leaked history:
|
||||
|
||||
- Unfocused editor: active line `-1`, all lines rendered.
|
||||
- Focused editor: selected line is source, inactive lines rendered.
|
||||
- Focus lost: active line returns to `-1`, all lines rendered.
|
||||
|
||||
This preserves D-008 hybrid editing while avoiding the previous initial-load ambiguity.
|
||||
|
||||
Invalidation behavior:
|
||||
|
||||
- Initial document load performs a full render with no active line.
|
||||
- Focus changes dirty only the previous and current active lines through the existing active-line invalidation path.
|
||||
- Typing still uses `DocumentLineIndexEdit` and dirty neighbor lines.
|
||||
- Checklist toggles update the Markdown checkbox range and rerender the affected task line while preserving selection and focus.
|
||||
- Scrolling does not trigger rendering; it uses TextKit layout caches for visible geometry.
|
||||
|
||||
Remaining boundary:
|
||||
|
||||
Presentation determinism is now covered at the attributed-string and TextKit line-fragment level for fixed-width snapshots. It is not yet covered by end-to-end pixel screenshots of the live `NSTextView` inside a real window. The remaining possible nondeterminism is therefore platform drawing outside the attributed/layout model, such as AppKit control rasterization or OS-level antialiasing. That is a narrower visual QA problem, not a render or presentation state-model problem.
|
||||
|
||||
## AttributedString and NSAttributedString
|
||||
|
||||
Swift `AttributedString` is useful for renderer-facing APIs and SwiftUI previews.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue