Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ All notable changes to this project will be documented in this file. Take a look
* `LCPService.init` now requires an explicit `deviceName` parameter. We recommend passing `UIDevice.current.name`. See [the migration guide](docs/Migration%20Guide.md).
* `LCPDialogAuthentication` no longer takes a `sender` view controller. It now presents its passphrase dialog through a new `LCPDialogAuthenticationDelegate` that you implement and retain for the lifetime of the authentication. See [the Readium LCP guide](docs/Guides/Readium%20LCP.md) and [the migration guide](docs/Migration%20Guide.md).

### Fixed

#### Navigator

* [#660](https://github.com/readium/swift-toolkit/issues/660) Fixed EPUB decorations using the `viewport` or `page` width being drawn under the notch or the home indicator in landscape orientation.

### Removed

* The deprecated `ReadiumAdapterGCDWebServer` and `ReadiumAdapterLCPSQLite` adapter packages have been removed.
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ final class EPUBReflowableSpreadView: EPUBSpreadView {
private var topConstraint: NSLayoutConstraint!
private var bottomConstraint: NSLayoutConstraint!

/// Chains the safe area insets updates sent to the JavaScript layer, to
/// preserve their scheduling order.
private var sendSafeAreaInsetsTask: Task<Void, Never>?

private static let reflowableScript = loadScript(named: "readium-reflowable")

required init(
Expand Down Expand Up @@ -108,6 +112,24 @@ final class EPUBReflowableSpreadView: EPUBSpreadView {
bottomConstraint.constant = -contentInset.bottom
scrollView.contentInset = .zero
}

// The top and bottom insets are applied natively to the web view, but
// the web view still spans the full width of the screen. We notify the
// JavaScript layer of the horizontal safe area insets, to prevent
// decorations spanning the full width of the viewport or page from
// being drawn under the notch in landscape orientation.
// See https://github.com/readium/swift-toolkit/issues/660
let script = """
readium.setSafeAreaInsets({'top': 0, 'left': \(Int(contentInset.left)), 'bottom': 0, 'right': \(Int(contentInset.right))});
"""
// The updates are chained to guarantee they reach the JavaScript
// layer in the order they were scheduled, so the latest insets
// always win. `updateContentInset()` can be called several times in
// a row during a rotation.
sendSafeAreaInsetsTask = Task { [previousTask = sendSafeAreaInsetsTask] in
await previousTask?.value
_ = await evaluateScript(script)
}
Comment on lines +129 to +132

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should [weak self] be used in case the view is no longer there while the task is still executing?

}

override func convertPointToNavigatorSpace(_ point: CGPoint) -> CGPoint {
Expand Down
72 changes: 66 additions & 6 deletions Sources/Navigator/EPUB/Scripts/src/decorator.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,58 @@ let styles = new Map();
let groups = new Map();
var lastGroupId = 0;

// Safe area insets of the viewport, in pixels.
//
// They are set by the native side, as the web view spans the full screen and
// its content can be partially obscured by the device notch or the home
// indicator, e.g. in landscape orientation.
let safeAreaInsets = { top: 0, right: 0, bottom: 0, left: 0 };

/**
* Sets the safe area insets of the viewport and relayouts the decorations
* to take them into account.
*
* Only the horizontal insets are currently used, as the vertical ones are
* already applied natively to the web view. The full insets shape is kept
* for symmetry with the fixed layout's `setViewport` API.
*/
export function setSafeAreaInsets(insets) {
if (
safeAreaInsets.top === insets.top &&
safeAreaInsets.right === insets.right &&
safeAreaInsets.bottom === insets.bottom &&
safeAreaInsets.left === insets.left
) {
return;
}
safeAreaInsets = insets;

groups.forEach(function (group) {
group.requestLayout();
});
}

/**
* Returns the horizontal safe area insets ({ left, right }) to apply to a
* page starting at `pageLeft` and spanning `pageWidth`, when the viewport
* holds `pagesPerViewport` pages side by side.
*
* The insets only apply to the pages touching the edges of the viewport,
* i.e. the first and last columns.
*/
function horizontalInsetsForPage(pageLeft, pageWidth, pagesPerViewport) {
// The page index is normalized to be positive, as RTL content uses
// negative coordinates in WKWebView.
const index =
((Math.round(pageLeft / pageWidth) % pagesPerViewport) + pagesPerViewport) %
pagesPerViewport;

return {
left: index === 0 ? safeAreaInsets.left : 0,
right: index === pagesPerViewport - 1 ? safeAreaInsets.right : 0,
};
}

/**
* Returns the document body's writing mode.
*/
Expand Down Expand Up @@ -234,6 +286,10 @@ export function DecorationGroup(groupId, groupName) {
const isVerticalLR = writingMode === "vertical-lr";

if (isVerticalRL || isVerticalLR) {
// Note that the safe area insets are intentionally ignored in
// vertical writing modes: the axis spanned by the "viewport" and
// "page" widths is the physical vertical one, which is already
// inset natively by the web view frame.
if (style.width === "wrap") {
element.style.width = `${rect.width}px`;
element.style.height = `${rect.height}px`;
Expand Down Expand Up @@ -290,21 +346,25 @@ export function DecorationGroup(groupId, groupName) {
element.style.left = `${rect.left + xOffset}px`;
element.style.top = `${rect.top + yOffset}px`;
} else if (style.width === "viewport") {
element.style.width = `${viewportWidth}px`;
element.style.height = `${rect.height}px`;
const left = Math.floor(rect.left / viewportWidth) * viewportWidth;
element.style.left = `${left + xOffset}px`;
const insets = horizontalInsetsForPage(left, viewportWidth, 1);
element.style.width = `${
viewportWidth - insets.left - insets.right
}px`;
element.style.height = `${rect.height}px`;
element.style.left = `${left + insets.left + xOffset}px`;
element.style.top = `${rect.top + yOffset}px`;
} else if (style.width === "bounds") {
element.style.width = `${boundingRect.width}px`;
element.style.height = `${rect.height}px`;
element.style.left = `${boundingRect.left + xOffset}px`;
element.style.top = `${rect.top + yOffset}px`;
} else if (style.width === "page") {
element.style.width = `${pageSize}px`;
element.style.height = `${rect.height}px`;
const left = Math.floor(rect.left / pageSize) * pageSize;
element.style.left = `${left + xOffset}px`;
const insets = horizontalInsetsForPage(left, pageSize, columnCount);
element.style.width = `${pageSize - insets.left - insets.right}px`;
element.style.height = `${rect.height}px`;
element.style.left = `${left + insets.left + xOffset}px`;
element.style.top = `${rect.top + yOffset}px`;
}
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/Navigator/EPUB/Scripts/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import {
setProperty,
setCSSProperties,
} from "./utils";
import { getDecorations, registerTemplates } from "./decorator";
import {
getDecorations,
registerTemplates,
setSafeAreaInsets,
} from "./decorator";

// Public API used by the navigator.
global.readium = {
Expand All @@ -36,6 +40,7 @@ global.readium = {
// decoration
registerDecorationTemplates: registerTemplates,
getDecorations: getDecorations,
setSafeAreaInsets: setSafeAreaInsets,

// DOM
findFirstVisibleLocator: findFirstVisibleLocator,
Expand Down