* feat(reader): slide and page curl turn animations (#555) Add an Animation Style setting (Push, Slide, Page Curl) next to the Paging Animation switch. Slide moves the turning page over the still previous or next page like the Apple Books slide; Page Curl folds it open in 3D so the page underneath is partially visible as it turns. Both styles track the finger: the page follows a horizontal drag and commits past halfway or on a flick, or settles back. The page header and footer stay in place while the page turns. The styles layer a View Transitions snapshot of the outgoing page over the live, stationary incoming page, since the pages of one section live in a single iframe and can never be on screen twice. They work for all writing modes including vertical-rl, and on engines without the View Transitions API (older WebViews) the paginator falls back to the existing push animation, so all platforms keep working page turns. The paginator changes live in the foliate-js submodule; this bumps the pointer, wires viewSettings.pageTurnStyle to the renderer turn-style attribute, and adds browser tests covering slide layering, curl, vertical-rl, finger tracking with commit and revert, and the push fallback. Fixes #555 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reader): add WebGL page curl renderer for mesh turn animations (#555) Grid mesh deformed around a cylinder: content past the fold wraps over and lands mirrored on top with a whitened page back, transparent where the page has curled away. Corner grabs start as a steep diagonal pinch that straightens as the turn completes so the whole page clears by the end. Groundwork for the Tauri mesh curl; capture and orchestration land separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(native-bridge): capture webview region as PNG on macOS and iOS (#555) New capture_webview_region plugin command returns a binary PNG snapshot of the calling webview (tauri::ipc::Response, no JSON overhead) for the mesh page-curl texture. macOS goes through WKWebView takeSnapshotWithConfiguration via with_webview on the main thread with a 500ms timeout; iOS snapshots in Swift and hands the PNG across the JSON-only plugin boundary base64-encoded, decoded back to bytes in mobile.rs. Windows, Linux, and Android reject for now so the JS side falls back to the CSS curl. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reader): drive the mesh page curl on Tauri platforms (#555) Wire the WebGL curl renderer and the native webview capture into page turns. A MeshCurlTurn controller runs the pipeline per turn: snapshot the content box, overlay the captured page drawn flat, turn the live view instantly underneath (the paginator's animated paths all gate on the animated attribute), then curl the capture away. Backward turns mirror the fold to the spine edge, matching the layered VT curl's old-page-recedes choreography. useMeshPageCurl wraps the view's prev/next so taps, keys, and wheel turns all curl, and registers a touch interceptor (between the reading ruler and the fixed-layout swipe) that scrubs the curl from the finger, committing past halfway or on a flick and otherwise un-curling and turning back under the overlay. The paginator stays out of the way via no-swipe while the mesh is active; if the native capture ever fails the session falls back to the paginator's CSS arc-fold curl and the shared applyPageTurnAttributes helper restores turn-style. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(build): restore iOS builds on Xcode 26.2 with a vendored swift-rs Swift 6.2's driver no longer honors swift-rs 1.0.7's cross-compilation style (swift build --arch <host> with per-swiftc -target overrides and an inherited SDKROOT): plugin sources compile against the wrong platform's Swift overlays and fail with baffling errors like type 'Bundle' has no member 'main' and extra argument 'privacy' in call. Upstream swift-rs is unmaintained, so vendor it under packages/swift-rs via a crates-io patch and build with SPM's first-class --triple/--sdk flags instead, dropping the leaked SDKROOT so the host-targeted manifest compile stays clean. Artifacts land in the unversioned-triple directory now, so the link search path follows. With --triple, SPM enforces the deployment floor declared in Package.swift (the old override bypassed it): bump native-bridge to iOS 15.0, matching the app's deployment target, since StoreKit's Storefront is used unguarded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(native-bridge): capture webview region on Android via PixelCopy (#555) Implements the Android side of capture_webview_region so the mesh page curl works there too. The Kotlin command scales the CSS-pixel rect by the display density, offsets it by the webview's window position, and reads the pixels back from the window surface with PixelCopy (API 26+, the app's minSdk), which includes the hardware-accelerated WebView that View.draw would miss. PNG encoding runs off the main thread and the result crosses the JSON plugin boundary base64-encoded, decoded back to bytes in mobile.rs like iOS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): right the upside-down page curl on iOS (#555) The renderer oriented its texture with UNPACK_FLIP_Y_WEBGL, which WebKit ignores for ImageBitmap uploads: on iOS the captured page rendered upside down, and the mirrored page back read as rotated 180 degrees instead of the ink-through-paper horizontal mirror Apple Books shows. Upload unflipped and sample page coordinates directly so no pixel-store flag is involved. The page texture in the browser test was only horizontally asymmetric, which is how the flip slipped through; it now uses four quadrants fed through the production PNG-blob-to-ImageBitmap path and pins the vertical orientation. Verified red/green by running the suite on Playwright WebKit, which reproduces the iOS behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): curl the whole page including header, footer, and margins (#555) The mesh curl captured only the margin-inset content box, leaving the running header, footer, and page margins static while just the text column turned. A physical page turn takes the whole sheet with it, as Apple Books does, so the capture and overlay now span the full reader cell. The overlay mounts above the in-cell header (z-10) and footer, so the static copies never show through the turning page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): gate layered View Transition turns and slide from a capture instead (#555) iOS 18 WebKit ships document.startViewTransition but crashes the WebContent process when a page-turn transition snapshots the reader, so the mere presence of the API is not enough for the layered slide/curl turns. Require nested view-transition groups (Chrome/WebView 140+) as the marker of a mature engine before setting turn-style on the renderer. Engines that fail the check no longer lose the slide on Tauri: the mesh curl's capture pipeline generalizes to CapturedPageTurn and now also drives a flat slide overlay (capture the outgoing page, turn instantly underneath, translate the captured page out toward the spine, mirrored for backward turns), clipped to the content box with an edge shadow like the VT slide. On the web, engines without full support fall back to push and the Slide/Page Curl options are hidden from the Animation Style select; a synced slide/curl setting from another device reads as Push there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): make the Android page curl start instantly (#555) The Android capture encoded a full-density PNG: 1080x2400 on a 3x Xiaomi 13 took ~1.5s per turn, so the page sat frozen long enough to read as the curl not working at all. Encode JPEG instead (the page is opaque) and cap the destination bitmap at 2x CSS pixels - PixelCopy scales into a smaller bitmap for free and the moving page stays sharp. Measured on device over CDP: the capture invoke drops from 1550ms to 34ms and the curl overlay mounts 132ms after the tap. The JS side stops hardcoding an image/png blob type and lets the decoder sniff the platform's actual format. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(reader): encode iOS page-curl captures as capped JPEG (#555) Apply the Android speedup to iOS: encode the snapshot as JPEG (the page is opaque) off the main thread, and cap it at 2x CSS pixels via WKSnapshotConfiguration.snapshotWidth on 3x screens, cutting both the encode time and the base64 payload crossing the JSON plugin boundary. The JS side already sniffs the image format from the bytes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
swift-rs
Call Swift functions from Rust with ease!
Setup
Add swift-rs to your project's dependencies and build-dependencies:
[dependencies]
swift-rs = "1.0.5"
[build-dependencies]
swift-rs = { version = "1.0.5", features = ["build"] }
Next, some setup work must be done:
- Ensure your swift code is organized into a Swift Package. This can be done in XCode by selecting File -> New -> Project -> Multiplatform -> Swift Package and importing your existing code.
- Add
SwiftRsas a dependency to your Swift package and make the build type.static.
let package = Package(
dependencies: [
.package(url: "https://github.com/Brendonovich/swift-rs", from: "1.0.5")
],
products: [
.library(
type: .static,
),
],
targets: [
.target(
// Must specify swift-rs as a dependency of your target
dependencies: [
.product(
name: "SwiftRs",
package: "swift-rs"
)
],
)
]
)
- Create a
build.rsfile in your project's root folder, if you don't have one already. - Use
SwiftLinkerin yourbuild.rsfile to link both the Swift runtime and your Swift package. The package name should be the same as is specified in yourPackage.swiftfile, and the path should point to your Swift project's root folder relative to your crate's root folder.
use swift_rs::SwiftLinker;
fn build() {
// swift-rs has a minimum of macOS 10.13
// Ensure the same minimum supported macOS version is specified as in your `Package.swift` file.
SwiftLinker::new("10.13")
// Only if you are also targetting iOS
// Ensure the same minimum supported iOS version is specified as in your `Package.swift` file
.with_ios("11")
.with_package(PACKAGE_NAME, PACKAGE_PATH)
.link();
// Other build steps
}
With those steps completed, you should be ready to start using Swift code from Rust!
If you experience the error dyld[16008]: Library not loaded: @rpath/libswiftCore.dylib
when using swift-rs with Tauri ensure you have set your
Tauri minimum system version
to 10.15 or higher in your tauri.config.json.
Calling basic functions
To allow calling a Swift function from Rust, it must follow some rules:
- It must be global
- It must be annotated with
@_cdecl, so that it is callable from C - It must only use types that can be represented in Objective-C,
so only classes that derive
NSObject, as well as scalars such as Int and Bool. This excludes strings, arrays, generics (though all of these can be sent with workarounds) and structs (which are strictly forbidden).
For this example we will use a function that simply squares a number:
public func squareNumber(number: Int) -> Int {
return number * number
}
So far, this function meets requirements 1 and 3: it is global and public, and only uses the Int type, which is Objective-C compatible.
However, it is not annotated with @_cdecl.
To fix this, we must call @_cdecl before the function's declaration and specify the name that the function is exposed to Rust with as its only argument.
To keep with Rust's naming conventions, we will export this function in snake case as square_number.
@_cdecl("square_number")
public func squareNumber(number: Int) -> Int {
return number * number
}
Now that squareNumber is properly exposed to Rust, we can start interfacing with it.
This can be done using the swift! macro, with the Int type helping to provide a similar function signature:
use swift_rs::swift;
swift!(fn square_number(number: Int) -> Int);
Lastly, you can call the function from regular Rust functions.
Note that all calls to a Swift function are unsafe,
and require wrapping in an unsafe {} block or unsafe fn.
fn main() {
let input: Int = 4;
let output = unsafe { square_number(input) };
println!("Input: {}, Squared: {}", input, output);
// Prints "Input: 4, Squared: 16"
}
Check the documentation for all available helper types.
Returning objects from Swift
Let's say that we want our squareNumber function to return not only the result, but also the original input.
A standard way to do this in Swift would be with a struct:
struct SquareNumberResult {
var input: Int
var output: Int
}
We are not allowed to do this, though, since structs cannot be represented in Objective-C.
Instead, we must use a class that extends NSObject:
class SquareNumberResult: NSObject {
var input: Int
var output: Int
init(_ input: Int, _ output: Int) {
self.input = input;
self.output = output
}
}
Yes, this class could contain the squaring logic too, but that is irrelevant for this example
An instance of this class can then be returned from squareNumber:
@_cdecl("square_number")
public func squareNumber(input: Int) -> SquareNumberResult {
let output = input * input
return SquareNumberResult(input, output)
}
As you can see, returning an NSObject from Swift isn't too difficult.
The same can't be said for the Rust implementation, though.
squareNumber doesn't actually return a struct containing input and output,
but instead a pointer to a SquareNumberResult stored somewhere in memory.
Additionally, this value contains more data than just input and output:
Since it is an NSObject, it contains extra data that must be accounted for when using it in Rust.
This may sound daunting, but it's not actually a problem thanks to SRObject<T>.
This type manages the pointer internally, and takes a generic argument for a struct that we can access the data through.
Let's see how we'd implement SquareNumberResult in Rust:
use swift_rs::{swift, Int, SRObject};
// Any struct that is used in a C function must be annotated
// with this, and since our Swift function is exposed as a
// C function with @_cdecl, this is necessary here
#[repr(C)]
// Struct matches the class declaration in Swift
struct SquareNumberResult {
input: Int,
output: Int
}
// SRObject abstracts away the underlying pointer and will automatically deref to
// &SquareNumberResult through the Deref trait
swift!(fn square_number(input: Int) -> SRObject<SquareNumberResult>);
Then, using the new return value is just like using SquareNumberResult directly:
fn main() {
let input = 4;
let result = unsafe { square_number(input) };
let result_input = result.input; // 4
let result_output = result.output; // 16
}
Creating objects in Rust and then passing them to Swift is not supported.
Optionals
swift-rs also supports Swift's nil type, but only for functions that return optional NSObjects.
Functions returning optional primitives cannot be represented in Objective C, and thus are not supported.
Let's say we have a function returning an optional SRString:
@_cdecl("optional_string")
func optionalString(returnNil: Bool) -> SRString? {
if (returnNil) return nil
else return SRString("lorem ipsum")
}
Thanks to Rust's null pointer optimisation,
the optional nature of SRString? can be represented by wrapping SRString in Rust's Option<T> type!
use swift_rs::{swift, Bool, SRString};
swift!(optional_string(return_nil: Bool) -> Option<SRString>)
Null pointers are actually the reason why a function that returns an optional primitive cannot be represented in C.
If this were to be supported, how could a nil be differentiated from a number? It can't!
Complex types
So far we have only looked at using primitive types and structs/classes,
but this leaves out some of the most important data structures: arrays (SRArray<T>) and strings (SRString).
These types must be treated with caution, however, and are not as flexible as their native Swift & Rust counterparts.
Strings
Strings can be passed between Rust and Swift through SRString, which can be created from native strings in either language.
As an argument
import SwiftRs
@_cdecl("swift_print")
public func swiftPrint(value: SRString) {
// .to_string() converts the SRString to a Swift String
print(value.to_string())
}
use swift_rs::{swift, SRString, SwiftRef};
swift!(fn swift_print(value: &SRString));
fn main() {
// SRString can be created by simply calling .into() on any string reference.
// This will allocate memory in Swift and copy the string
let value: SRString = "lorem ipsum".into();
unsafe { swift_print(&value) }; // Will print "lorem ipsum" to the console
}
As a return value
import SwiftRs
@_cdecl("get_string")
public func getString() -> SRString {
let value = "lorem ipsum"
// SRString can be created from a regular String
return SRString(value)
}
use swift_rs::{swift, SRString};
swift!(fn get_string() -> SRString);
fn main() {
let value_srstring = unsafe { get_string() };
// SRString can be converted to an &str using as_str()...
let value_str: &str = value_srstring.as_str();
// or though the Deref trait
let value_str: &str = &*value_srstring;
// SRString also implements Display
println!("{}", value_srstring); // Will print "lorem ipsum" to the console
}
Arrays
Primitive Arrays
Representing arrays properly is tricky, since we cannot use generics as Swift arguments or return values according to rule 3.
Instead, swift-rs provides a generic SRArray<T> that can be embedded inside another class that extends NSObject that is not generic,
but is restricted to a single element type.
import SwiftRs
// Argument/Return values can contain generic types, but cannot be generic themselves.
// This includes extending generic types.
class IntArray: NSObject {
var data: SRArray<Int>
init(_ data: [Int]) {
self.data = SRArray(data)
}
}
@_cdecl("get_numbers")
public func getNumbers() -> IntArray {
let numbers = [1, 2, 3, 4]
return IntArray(numbers)
}
use swift_rs::{Int, SRArray, SRObject};
#[repr(C)]
struct IntArray {
data: SRArray<Int>
}
// Since IntArray extends NSObject in its Swift implementation,
// it must be wrapped in SRObject on the Rust side
swift!(fn get_numbers() -> SRObject<IntArray>);
fn main() {
let numbers = unsafe { get_numbers() };
// SRArray can be accessed as a slice via as_slice
let numbers_slice: &[Int] = numbers.data.as_slice();
assert_eq!(numbers_slice, &[1, 2, 3, 4]);
}
To simplify things on the rust side, we can actually do away with the IntArray struct.
Since IntArray only has one field, its memory layout is identical to that of SRArray<usize>,
so our Rust implementation can be simplified at the cost of equivalence with our Swift code:
// We still need to wrap the array in SRObject since
// the wrapper class in Swift is an NSObject
swift!(fn get_numbers() -> SRObject<SRArray<Int>>);
NSObject Arrays
What if we want to return an NSObject array? There are two options on the Swift side:
- Continue using
SRArrayand a custom wrapper type, or - Use
SRObjectArray, a wrapper type provided byswift-rsthat accepts anyNSObjectas its elements. This can be easier than continuing to create wrapper types, but sacrifices some type safety.
There is also SRObjectArray<T> for Rust, which is compatible with any single-element Swift wrapper type (and of course SRObjectArray in Swift),
and automatically wraps its elements in SRObject<T>, so there's very little reason to not use it unless you really like custom wrapper types.
Using SRObjectArray in both Swift and Rust with a basic custom class/struct can be done like this:
import SwiftRs
class IntTuple: NSObject {
var item1: Int
var item2: Int
init(_ item1: Int, _ item2: Int) {
self.item1 = item1
self.item2 = item2
}
}
@_cdecl("get_tuples")
public func getTuples() -> SRObjectArray {
let tuple1 = IntTuple(0,1),
tuple2 = IntTuple(2,3),
tuple3 = IntTuple(4,5)
let tupleArray: [IntTuple] = [
tuple1,
tuple2,
tuple3
]
// Type safety is only lost when the Swift array is converted to an SRObjectArray
return SRObjectArray(tupleArray)
}
use swift_rs::{swift, Int, SRObjectArray};
#[repr(C)]
struct IntTuple {
item1: Int,
item2: Int
}
// No need to wrap IntTuple in SRObject<T> since
// SRObjectArray<T> does it automatically
swift!(fn get_tuples() -> SRObjectArray<IntTuple>);
fn main() {
let tuples = unsafe { get_tuples() };
for tuple in tuples.as_slice() {
// Will print each tuple's contents to the console
println!("Item 1: {}, Item 2: {}", tuple.item1, tuple.item2);
}
}
Complex types can contain whatever combination of primitives and SRObject<T> you like, just remember to follow the 3 rules!
Bonuses
SRData
A wrapper type for SRArray<T> designed for storing u8s - essentially just a byte buffer.
Tighter Memory Control with autoreleasepool!
If you've come to Swift from an Objective-C background, you likely know the utility of @autoreleasepool blocks.
swift-rs has your back on this too, just wrap your block of code with a autoreleasepool!, and that block of code now executes with its own autorelease pool!
use swift_rs::autoreleasepool;
for _ in 0..10000 {
autoreleasepool!({
// do some memory intensive thing here
});
}
Limitations
Currently, the only types that can be created from Rust are number types, boolean, SRString, and SRData.
This is because those types are easy to allocate memory for, either on the stack or on the heap via calling out to swift,
whereas other types are not. This may be implemented in the future, though.
Mutating values across Swift and Rust is not currently an aim for this library, it is purely for providing arguments and returning values. Besides, this would go against Rust's programming model, potentially allowing for multiple shared references to a value instead of interior mutability via something like a Mutex.
License
Licensed under either of
- Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.