iOS: paint the page spacers natively while pageSpacersColor is set - #16
Merged
Merged
Conversation
…handler Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVfQ8bBXbRyu9DdJQSmJge
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVfQ8bBXbRyu9DdJQSmJge
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVfQ8bBXbRyu9DdJQSmJge
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVfQ8bBXbRyu9DdJQSmJge
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVfQ8bBXbRyu9DdJQSmJge
…ompiles Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVfQ8bBXbRyu9DdJQSmJge
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
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 namedpageSpacersand hosts an overlay view inside the WKWebView's own scroll view. Unsetting the color removes both, the same waypageCurlEnabledcreates and destroys the curl handler, so a webview without the prop never exposes the painter to its page. The stored color survivesdestroyWebView, 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:
pageSpacersVerticalStartOffsetmoves the top edge down andpageSpacersVerticalEndOffsetmoves 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.pageSpacersDebugLogginggates the count and paint time log, likepageCurlDebugLogging.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