Summary
The markdown editor should expose a supported way to render an embedder-supplied header view above the editable body — a region that the host app fills with arbitrary content, that the engine reserves space for, offsets the text below, keeps hit-testable, and (optionally) scrolls away or collapses.
A prototype exists on feature/inline-scrolling-header (#61), but it grew out of one specific consumer (Nodes' inline node inspector) and its shape, naming, and guarantees are still Nodes-flavored. This issue is to generalize it into a stable, documented API that stands on its own, independent of any single app.
Why this belongs in the engine, not the app
The header is not just "a view on top." To behave correctly it must participate in the engine's internals:
- The body text must be shifted down by the reserved height (
textContainerOrigin / content-height / scroll-range accounting).
- Clicks, caret placement, selection, and link/coordinate mapping must all stay correct relative to that offset.
- It must remain hit-testable and stable during scroll (we hit real AppKit compositing/layout issues here — see below).
An app cannot get these right by stacking a view over the editor from the outside; they were exactly the failure modes of the earlier overlay/injection attempts. The reservation + coordinate-space integration only works from inside the engine, so the capability should be a real engine feature with a clean contract.
Use cases (beyond Nodes)
A reusable header unlocks, for any embedder:
- Document title / hero + metadata — styled H1 + author/date/byline above the body.
- Frontmatter / properties panel — render YAML/frontmatter as a card (Obsidian/Notion-style properties).
- Status & context banners — "Draft", "Read-only", "AI-generated", sync state, conflict warnings.
- Breadcrumbs / file-path bar for navigation.
- Cover image / hero media that scrolls away as you read.
- A scroll-away table of contents or reading-progress strip.
- Per-document toolbars / inspectors (the Nodes case — just one instance of the general pattern).
Proposed capability
- Accept an arbitrary embedder view (SwiftUI hosted via
NSHostingView, or a raw NSView).
- Behaviors, ideally selectable:
- scroll-away (moves with the content — current prototype), and later
- pinned/sticky (stays at the top while the body scrolls under it).
- Collapsible with animation, between a collapsed height and the content's intrinsic height.
- Height is layout-driven (the embedder's own content size), with an embedder-provided collapsed height.
- Plays correctly with safe-area / content insets.
- Clean lifecycle: rebuild on document switch, teardown, no retain cycles.
Rough shape (illustrative, not final):
struct EditorHeader {
var content: AnyView // or an NSView factory
var mode: Mode // .scrollAway (+ later .pinned)
var collapsedHeight: CGFloat
var isExpanded: Bool
}
// MarkdownEditorView(..., header: EditorHeader?)
Hard problems already surfaced (carry these into the API)
From building the prototype (#61), these must be part of the contract/tests, not rediscovered per-consumer:
- Reserved height must come from a stable source. Driving the body's top inset from the live, in-flight clip frame causes the body to slide under the header during scroll (autoresizing text view vs. Auto-Layout-pinned clip race). Drive it from the intended constant.
- Cross-tree compositing during scroll. A layer-backed header (e.g.
NSHostingView) inside a non-layer-backed, transparent NSTextView desyncs at the header/body seam during live scroll — the body shows through the header's lower rows for a frame. Fix in the prototype was to layer-back the document view; this needs validation across TextKit2 caret/IME/selection/overlay drawing as a supported configuration.
- Caret/coordinate workarounds (
NativeTextView+CaretWorkarounds) compute the insertion-indicator Y without adding the reserved top inset — needs auditing so a reserved header never mis-places the trailing-newline caret.
- Hit-testing: the header must live inside the text view's bounds so AppKit hit-tests it (out-of-bounds subviews draw but don't receive events).
Acceptance criteria
Out of scope / follow-ups
- Pinned/sticky mode (start with scroll-away).
- Multiple stacked headers / footer regions.
Prototype: #61.
Summary
The markdown editor should expose a supported way to render an embedder-supplied header view above the editable body — a region that the host app fills with arbitrary content, that the engine reserves space for, offsets the text below, keeps hit-testable, and (optionally) scrolls away or collapses.
A prototype exists on
feature/inline-scrolling-header(#61), but it grew out of one specific consumer (Nodes' inline node inspector) and its shape, naming, and guarantees are still Nodes-flavored. This issue is to generalize it into a stable, documented API that stands on its own, independent of any single app.Why this belongs in the engine, not the app
The header is not just "a view on top." To behave correctly it must participate in the engine's internals:
textContainerOrigin/ content-height / scroll-range accounting).An app cannot get these right by stacking a view over the editor from the outside; they were exactly the failure modes of the earlier overlay/injection attempts. The reservation + coordinate-space integration only works from inside the engine, so the capability should be a real engine feature with a clean contract.
Use cases (beyond Nodes)
A reusable header unlocks, for any embedder:
Proposed capability
NSHostingView, or a rawNSView).Rough shape (illustrative, not final):
Hard problems already surfaced (carry these into the API)
From building the prototype (#61), these must be part of the contract/tests, not rediscovered per-consumer:
NSHostingView) inside a non-layer-backed, transparentNSTextViewdesyncs at the header/body seam during live scroll — the body shows through the header's lower rows for a frame. Fix in the prototype was to layer-back the document view; this needs validation across TextKit2 caret/IME/selection/overlay drawing as a supported configuration.NativeTextView+CaretWorkarounds) compute the insertion-indicator Y without adding the reserved top inset — needs auditing so a reserved header never mis-places the trailing-newline caret.Acceptance criteria
Out of scope / follow-ups
Prototype: #61.