Skip to content

iOS: paint the page spacers natively while pageSpacersColor is set - #16

Merged
artemlitch merged 11 commits into
masterfrom
artem/page-borders-native
Sep 16, 2026
Merged

artemlitch merged 11 commits into
masterfrom
artem/page-borders-native

Conversation

@artemlitch

@artemlitch artemlitch commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

Injected pagination writes spacers into the document to separate pages, but only the webview knows where they are. Nothing native can draw against them, because the only way out of the page is the React Native message bridge, which goes through JS.

Change

A new iOS prop pageSpacersColor (ColorValue) turns the feature on. While it is set, the webview registers a WKScriptMessageHandler named pageSpacers and hosts an overlay view inside the WKWebView's own scroll view. Unsetting the color removes both, the same way pageCurlEnabled creates and destroys the curl handler, so a webview without the prop never exposes the painter to its page. The stored color survives destroyWebView, because Fabric leaves a recycled host's old props in place and a remount with the same color never runs the setter again.

The page posts { spacers: [{ top, height }] } in CSS pixels from the top of the document. The handler validates the message, then paints one full-width CALayer per spacer on the overlay. UIKit moves the overlay with the content on every frame, so there is no delegate callback or per-frame sync that could let the bands drift from the text. Each post replaces the previous set, reusing the existing layers, with implicit animations disabled. A new source clears the overlay. Changing the color recolors the layers in place. A message that arrives after the overlay is gone is dropped; only a bad payload warns.

Two more props trim each band: pageSpacersVerticalStartOffset moves the top edge down and pageSpacersVerticalEndOffset moves the bottom edge up, both taken off the height, so highlight outlines at the edge of a page are not covered. A spacer shorter than the offsets paints nothing. pageSpacersDebugLogging gates the count and paint time log, like pageCurlDebugLogging.

Everything goes through the Fabric codegen spec, the old-arch view manager, the TS types and the committed lib, following the page curl props. Android gets the no-op setters the generated interface requires.

Verified

Opened a book on an iPhone 17 Pro simulator with the content frame side of this change. One post per open, 69 spacers painted in 0.2 ms. With a red fill the bands sit exactly on the page gaps, chunk ends included, and follow the content through page turns. Measured in raw screenshots at 3x: a 3 pt start offset moved a band's top edge down 9 px and left its bottom edge in place, the end offset did the reverse, and changing either prop moved the live layers without a repaint. Switching to continuous scroll removed the handler and the overlay; switching back repainted from the new plan. Reopening a document after a remount, which recycles the host, repainted. Bookwise passes the reader background color and 3 pt on both ends, so in normal use the layers are invisible.

The content frame and Bookwise side live in the rekindled worktree and are not part of this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FVfQ8bBXbRyu9DdJQSmJge

@artemlitch artemlitch changed the title iOS: receive page borders from the content frame on a native message handler iOS: paint the gaps between paginated pages natively from the content frame's page borders Sep 15, 2026
@artemlitch artemlitch changed the title iOS: paint the gaps between paginated pages natively from the content frame's page borders iOS: paint the injected page spacers natively from the content frame Sep 16, 2026
artemlitch and others added 2 commits September 15, 2026 21:41
Rename the page borders prop and handler to page spacers, which is what the
content frame sends. The handler and its overlay are now created with the
color and torn down with it, the way the page curl handler is, so a webview
without the prop never exposes the painter. The painter validates the
message before reading it, reuses its views between posts, clears on a new
source, and logs through RCTLog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVfQ8bBXbRyu9DdJQSmJge
@artemlitch artemlitch changed the title iOS: paint the injected page spacers natively from the content frame iOS: paint the page spacers natively while pageSpacersColor is set Sep 16, 2026
artemlitch and others added 4 commits September 16, 2026 09:29
Each spacer starts that many points lower and loses the same amount of
height, so its bottom edge stays put and highlight outlines at the top of
a page are not covered. Changing it moves the painted views in place.

Also keep pageSpacersColor across destroyWebView. Fabric leaves a recycled
host's old props in place, so a remount with the same color never runs the
setter again and setUp needs the stored value to register the handler.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVfQ8bBXbRyu9DdJQSmJge
Each spacer is now a CALayer on the overlay instead of a UIView, so the
bands stay out of the hit-test and accessibility trees. Layer edits run
with implicit actions disabled so a repaint or a prop change never
animates.

pageSpacersVerticalEndOffset pulls each spacer's bottom edge up the way
the start offset pushes its top edge down. pageSpacersDebugLogging gates
the paint log the way pageCurlDebugLogging does. A message that arrives
after the overlay is gone is dropped quietly; only a bad payload warns.
Disabling no longer depends on the webview still existing. The prop docs
now state the message shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVfQ8bBXbRyu9DdJQSmJge
The offset setters adjusted live layers by a delta and clamped at zero,
which lost the original height once an offset exceeded it. The painter now
keeps the last well-formed spacer list and one layout routine serves the
message, the color and both offsets.

pageSpacersColor is assigned on every prop update and cleared on
destroyWebView, the way pageCurlEnabled is handled, so a recycled host
reaches the right state whether or not Fabric keeps its old props.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVfQ8bBXbRyu9DdJQSmJge
@artemlitch
artemlitch merged commit bb5b88d into master Sep 16, 2026
6 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant