Repository: flutter
Issue: Flutter's text rendering has different letter spacing from iOS native
Labels: platform-ios,framework,engine,a: fidelity,f: cupertino,a: typography,P2,team-ios,triaged-ios,fyi-text-input

Flutter's `Text()` has noticeably different letter spacing from iOS's `Text()` by default.

### Background

iOS renders text with "SF Pro Text" font for font size <20, and "SF Pro Display" font for font size >= 20. In Flutter they're called "CupertinoSystemText" and "CupertinoSystemDisplay" respectively.

### Comparison with current Flutter
(light grey Flutter, black native iOS)
<img width="677" alt="image" src="https://github.com/flutter/flutter/assets/1596656/55c1c37c-ee87-4f37-add9-f21d1e2265ff">

### Adjustment
 I tried to apply `letterSpacing` to Flutter's text to get a result close to the native one. The needed adjustment turns out to be a non-linear curve, which is interpolated as a cubic polynomial.

<img width="784" alt="image" src="https://github.com/flutter/flutter/assets/1596656/24c5151b-f0b5-4488-a6ea-f2f858329885">
<img width="756" alt="image" src="https://github.com/flutter/flutter/assets/1596656/52349b27-f88f-42c8-9b5c-b52940ac8268">

### Comparison after adjustment

(light grey Flutter, black native iOS)
<img width="673" alt="image" src="https://github.com/flutter/flutter/assets/1596656/fc609038-f505-4d0e-8696-cdf4620fa5c8">

(Yes, I know line height at small font size still deviates noticeably.)

### Note
* If I understand correctly, Flutter's text rendering is handled by Skia, which delegates text shaping to HarfBuzz. There's an issue on HarfBuzz ([link](https://github.com/harfbuzz/harfbuzz/issues/1995)), which complained about HarfBuzz's text tracking deviates from that of macOS in 2019. Although I don't quite get the content in that page and whether the issue was considered fixed.

### Reproduction code

<details>
<summary>Dart code</summary>

```dart
import 'dart:math';
import 'package:flutter/cupertino.dart';

void main() => runApp(const Main());

class Main extends StatelessWidget {
  const Main({super.key});

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      child: SafeArea(
        child: Center(
          child: Transform.translate(
            offset: const Offset(0, -0),
            child: Container(
              width: 400,
              height: 500,
              decoration: BoxDecoration(
                border: Border.all(),
              ),
              child: Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    ...<double>[3, 4, 5, 7, 8, 9, 10, 11, 13, 15, 17, 19].map(
                      (double i) => Text(
                        'CupertinoSystemText size ${i.round()}',
                        style: TextStyle(
                          fontFamily: 'CupertinoSystemText',
                          fontSize: i,
                          letterSpacing: smallFontSpacingMap(i),
                        ),
                      )
                    ),
                    ...<double>[20, 22, 25, 28, 32, 36].map(
                      (double i) => Text(
                        'SystemDisplay size ${i.round()}',
                        style: TextStyle(
                          fontFamily: 'CupertinoSystemDisplay',
                          fontSize: i,
                          letterSpacing: bigFontSpacingMap(i),
                        ),
                      )
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

double smallFontSpacingMap(double i) {
  return switch (i) {
    // Experimental value
    3 => 0.11,
    4 => 0.16,
    5 => 0.178,
    7 => 0.2,
    8 => 0.173,
    9 => 0.142,
    10 => 0.1,
    11 => 0.03,
    13 => -0.05,
    15 => -0.21,
    17 => -0.44,
    19 => -0.54,
    // Interpolation curve
    _ => -0.158 + 0.123 * i * (1 - 0.0983 * i * (1 - 0.0195 * i)),
  };
}

double bigFontSpacingMap(double i) {
  return switch (i) {
    // Experimental value
    20 => 0.395,
    22 => 0.325,
    25 => 0.26,
    28 => 0.34,
    32 => 0.355,
    36 => 0.335,
    // Interpolation curve
    _ => ((double j) =>
            7.05 - 0.73 * i * (1 - 0.0353 * i * (1 - 0.0116 * i))
          )(min(36, i)), // Extrapolate flatly above 36
  };
}
```

</details>
<details>
<summary>SwiftUI code</summary>

```swift
import SwiftUI

struct ContentView: View {
    var body: some View {     
            Rectangle()
                .fill(Color.white)
                .frame(width: 400, height: 500)
                .overlay(
                    VStack {
                        ForEach([3, 4, 5, 7, 8, 9, 10, 11, 13, 15, 17, 19], id: \.self) { i in
                            Text(String(format: "CupertinoSystemText size %d", Int(i)))
                                .font(.system(size: i))
                                .foregroundColor(.black)

                        }
                        ForEach([20, 22, 25, 28, 32, 36], id: \.self) { i in
                            Text(String(format: "SystemDisplay size %d", Int(i)))
                                .font(.system(size: i))
                                .foregroundColor(.black)

                        }
                    }
                )
    }
}

#Preview {
    ContentView()
}
```
</details>