Reads the operating system text size setting in Tauri 2.12 (or later) apps and reports changes to it.
The plugin reports the text size as a scale. The scale is 1 at the platform's default
text size. A larger text size gives a scale above 1, and a smaller text size gives a
scale below 1. An app multiplies its own text sizes by the scale, for example through
a CSS custom property.
- Read the current text scale with one command.
- Receive each change through one Tauri event, from each platform that reads a text size.
- Build and run on every Tauri target. A platform that has no text size, or that the
plugin does not support yet, returns
1and never emits the event.
| Platform | Source of the scale |
|---|---|
| Windows | UISettings.TextScaleFactor |
| Linux | 1. A later version can read the GTK text scale |
| macOS | 1. macOS has no public API for its text size |
| Android | Configuration.fontScale |
| iOS | UIFont.preferredFont: the body font size, divided by its default size |
Important
An Android app must change its own Android project for the plugin to work fully. See Android app setup.
The plugin is one crate. Unlike tauri-plugin-connectivity, it has no
Tauri-independent crate. Each platform implementation reads one operating system value,
such as Configuration.fontScale, and needs the Tauri runtime to emit the change event.
A platform that reads a text size emits the tauri-plugin-text-scale:changed event from
Rust to every window. The frontend listens for the same event on every platform, and a
window that opens later receives the same changes.
-
Install the npm dependencies:
npm install
-
Build the TypeScript bindings:
npm run build
-
Build the Rust plugin:
cargo build
Run all tests (TypeScript and Rust):
npm testRun the TypeScript tests only:
npm run test:tsRun the Rust tests only:
cargo test --workspace --libRun the Kotlin tests for the Android text scale logic (requires JDK 17 and the Android SDK):
cd android && ./gradlew :lib:testThe Kotlin tests cover the logic in android/lib, which does not depend on the Tauri
Android API, so the tests run without an emulator or a Tauri app build.
Run the Swift tests for the iOS text scale logic (requires Xcode):
swift test --package-path ios/TextScaleCoreThe Swift tests cover the logic in ios/TextScaleCore, which does not depend on UIKit or
the Tauri iOS API, so the tests run on macOS without a simulator.
See Android text scale manual testing for the device setup and the scenarios: the first read, a change while the app runs, rotation, the WebView text zoom and activity recreation.
See iOS text scale manual testing for the simulator setup and the scenarios: the first read, a change while the app runs, every text size, a change in the background and the WebView text size.
See Windows text scale manual testing for the setup and the scenarios: the first read, a change while the app runs, the smallest and largest text size, the WebView text size and a full-trust MSIX package.
The example app shows the current scale and lists each change that the plugin reports.
This plugin requires a Rust version of at least 1.94.0
Add the plugin to your Cargo.toml. Pin it to a commit with rev:
src-tauri/Cargo.toml
[dependencies]
tauri-plugin-text-scale = { git = "https://github.com/silvermine/tauri-plugin-text-scale", rev = "<sha>" }The package is not on the npm registry. Install the JavaScript bindings from GitHub, at the same commit as the Rust crate:
npm install github:silvermine/tauri-plugin-text-scale#<sha>An Android app needs the changes below in its generated Android project, under
src-tauri/gen/android. The plugin cannot make them, because they are in files that the
app owns. If you run tauri android init again, make sure that the changes are still
there.
The plugin's Android module sets compileSdk to 37. An app that builds it needs the
Android toolchain from Tauri 2.12 (or later) tauri android init:
- Android Gradle Plugin (AGP) 9.1.1 or later. API 37 needs AGP 9.1.1. Tauri 2.12 uses AGP 9.3.1.
- Gradle 9.3.1 or later. AGP 9.1 needs Gradle 9.3.1. Tauri 2.12 uses Gradle 9.6.1.
- Kotlin Gradle Plugin 1.9 or 2.x. The module uses
compilerOptions, notkotlinOptions.
An Android project from an older Tauri template uses AGP 8, and it cannot build the
module. A new tauri crate does not change that project. Upgrade AGP and Gradle in
src-tauri/gen/android, or run tauri android init again with the Tauri 2.12 CLI and
then make the two changes below.
In src-tauri/gen/android/app/src/main/AndroidManifest.xml, add fontScale to the
android:configChanges attribute of the main activity:
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|fontScale"
...>If the app has more activities that show a WebView, add fontScale to each of them.
Why: when a phone setting changes, Android recreates the activity. That is, it destroys
the app's screen and builds a new one. An activity that lists the setting in
configChanges is kept instead. The Tauri template does not list fontScale.
- With
fontScalelisted, the app keeps running as it is. Android reports the new font size, and the plugin emitstauri-plugin-text-scale:changed. - Without it, the page loads again and loses its state, for example the scroll
position. The plugin emits no event. The new page reads the new scale with
getTextScale().
Add this override to MainActivity, in
src-tauri/gen/android/app/src/main/java/<your package path>/MainActivity.kt:
import android.webkit.WebView
class MainActivity : TauriActivity() {
override fun onWebViewCreate(webView: WebView) {
// Turn off the WebView text zoom. The app applies the text scale itself.
webView.settings.textZoom = 100
}
}If MainActivity already overrides onWebViewCreate, add the textZoom line to it.
Why: the Android WebView has its own text zoom. Android sets it from the font size, and
it multiplies all text in the page by the font scale. An app that also applies the scale
from this plugin then scales its text twice, for example 1.3 × 1.3 at a font scale of
1.3. CSS cannot turn the text zoom off, because text-size-adjust has no effect on it.
textZoom = 100 turns it off.
The plugin sets textZoom = 100 on the first WebView, but it cannot reach a later one:
- Tauri gives a plugin only the first WebView, through
load. - Android makes a new WebView each time it recreates the activity. This happens after
a change to a setting that
configChangesdoes not list, such as Bold text. - Wry, the Tauri library that makes the WebViews, calls
onWebViewCreatein the app's activity for every WebView. So the app's activity is the only place that reaches each one.
Without this override, the text has the right size until Android recreates the activity. After that, all text is scaled twice until the user closes and opens the app again.
The plugin needs iOS 15 or later. Tauri's default minimum is iOS 14, so set the minimum
in src-tauri/tauri.conf.json if the app does not set iOS 15 or later already:
{
"bundle": {
"iOS": {
"minimumSystemVersion": "15.0"
}
}
}The app needs no other change. The iOS WebView does not scale text by itself, so the app applies the scale once, where it chooses.
Initialize the plugin in your tauri::Builder:
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_text_scale::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Add the default permission to a capability for each window that reads the scale:
src-tauri/capabilities/default.json
{
"identifier": "default",
"windows": ["main"],
"permissions": [
"core:default",
"text-scale:default"
]
}text-scale:default allows the get_text_scale command. The change event needs no
permission of its own, because core:default already allows listening to events.
import { getTextScale } from '@silvermine/tauri-plugin-text-scale';
async function applyTextScale(): Promise<void> {
const scale = await getTextScale();
document.documentElement.style.setProperty('--os-text-scale', String(scale));
}If the platform fails to report its text size, getTextScale() rejects with a string
error.
import { onTextScaleChanged } from '@silvermine/tauri-plugin-text-scale';
const unlisten = await onTextScaleChanged((scale) => {
document.documentElement.style.setProperty('--os-text-scale', String(scale));
});
// Later, to stop listening:
unlisten();Subscribe before the first read, so that the listener receives a change that arrives between the two calls. A change event can also arrive while the read waits for its reply. The event is then newer than the reply, so keep the event's value:
import { getTextScale, onTextScaleChanged } from '@silvermine/tauri-plugin-text-scale';
function setScale(scale: number): void {
document.documentElement.style.setProperty('--os-text-scale', String(scale));
}
async function followTextScale(): Promise<void> {
let changed = false;
await onTextScaleChanged((scale) => {
changed = true;
setScale(scale);
});
const scale = await getTextScale();
if (!changed) {
setScale(scale);
}
}Apps that keep their own manifest of backend events can import the event name instead of typing it again:
import { TEXT_SCALE_CHANGED_EVENT } from '@silvermine/tauri-plugin-text-scale';The payload of the event is the new scale as a number.
Read the scale from any Tauri manager type through the extension trait:
use tauri_plugin_text_scale::TextScaleExt;
fn log_text_scale<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
match app.text_scale().scale() {
Ok(scale) => println!("Text scale: {scale}"),
Err(error) => eprintln!("Could not read the text scale: {error}"),
}
}On iOS, the read fails before the app creates its first WebView, because the plugin
reads the text size when Tauri gives it a WebView. Tauri creates the windows from
tauri.conf.json before it runs the app's setup, so a read in setup works in an app
that has such a window.
This project follows the Silvermine standardization guidelines. Key standards include:
- Markdownlint: Markdown linting for documentation
- Commitlint: Conventional commit message format
- Code Style: 3-space indentation, LF line endings
npm run standardsRust dependencies are checked with cargo-deny on every push and pull request. The example app has its own lockfile and is not checked. Run the same check locally:
cargo install cargo-deny@0.20.2 --locked
cargo deny --workspace --all-features --locked checkFix advisories by updating the dependency where possible. Any exception in deny.toml
must say why an update cannot resolve it.
MIT
Contributions are welcome! Please follow the established coding standards and commit message conventions.