@momo-kits/native-kits 0.162.2-test.1-debug → 0.162.2-test.10-debug
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codegraph/.gitignore +5 -0
- package/compose/build.gradle.kts +1 -1
- package/compose/build.gradle.kts.backup +4 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +12 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +6 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +30 -28
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +45 -2
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +2 -0
- package/ios/Application/FontScaleStore.swift +77 -0
- package/ios/Typography/Text.swift +19 -7
- package/ios/Typography/Typography.swift +21 -8
- package/package.json +1 -1
package/compose/build.gradle.kts
CHANGED
|
@@ -40,7 +40,7 @@ kotlin {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
cocoapods {
|
|
43
|
-
version =
|
|
43
|
+
version = gitlabVersion
|
|
44
44
|
summary = "IOS Shared module"
|
|
45
45
|
homepage = "https://momo.vn"
|
|
46
46
|
ios.deploymentTarget = "15.0"
|
|
@@ -86,14 +86,14 @@ kotlin {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
android {
|
|
89
|
-
namespace = "
|
|
90
|
-
compileSdk =
|
|
89
|
+
namespace = "$gitlabGroup.$gitlabArtifactId"
|
|
90
|
+
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
|
91
91
|
compileOptions {
|
|
92
92
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
93
93
|
targetCompatibility = JavaVersion.VERSION_17
|
|
94
94
|
}
|
|
95
95
|
defaultConfig {
|
|
96
|
-
minSdk =
|
|
96
|
+
minSdk = libs.versions.android.minSdk.get().toInt()
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -28,8 +28,7 @@ data class MiniAppContext(
|
|
|
28
28
|
val providerId: String = "",
|
|
29
29
|
val permissions: List<Map<String, Any>>? = emptyList(),
|
|
30
30
|
val features: FeatureFlags? = null,
|
|
31
|
-
val
|
|
32
|
-
val appFontScale: Float? = null,
|
|
31
|
+
val fontScaleConfig: FontScaleConfig? = null,
|
|
33
32
|
) {
|
|
34
33
|
companion object {
|
|
35
34
|
|
|
@@ -61,8 +60,7 @@ data class MiniAppContext(
|
|
|
61
60
|
providerId = parent.providerId.ifBlank { child.providerId },
|
|
62
61
|
permissions = if (!parent.permissions.isNullOrEmpty()) parent.permissions else child.permissions,
|
|
63
62
|
features = mergeFeatureFlags(parent.features, child.features),
|
|
64
|
-
|
|
65
|
-
appFontScale = parent.appFontScale ?: child.appFontScale,
|
|
63
|
+
fontScaleConfig = parent.fontScaleConfig ?: child.fontScaleConfig,
|
|
66
64
|
)
|
|
67
65
|
}
|
|
68
66
|
|
|
@@ -108,6 +106,14 @@ val LocalComponentInformation = staticCompositionLocalOf<ComponentInformation?>
|
|
|
108
106
|
null
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
|
|
109
|
+
@Immutable
|
|
110
|
+
data class FontScaleConfig(
|
|
111
|
+
val useOSFontScale: Boolean = true,
|
|
112
|
+
val userScaleRate: Float? = null,
|
|
113
|
+
// Host-measured OS font scale, pushed live on resume/config change (MainActivity has
|
|
114
|
+
// configChanges=fontScale so LocalDensity.fontScale can be stale). Falls back to density when null.
|
|
115
|
+
val osFontScale: Float? = null,
|
|
116
|
+
)
|
|
112
117
|
|
|
113
|
-
|
|
118
|
+
// Single source of truth: holds both the OS flag and the user-adjusted rate.
|
|
119
|
+
val LocalFontScaleConfig = staticCompositionLocalOf { FontScaleConfig() }
|
|
@@ -40,6 +40,7 @@ fun Text(
|
|
|
40
40
|
softWrap: Boolean = true,
|
|
41
41
|
accessibilityId: String? = null,
|
|
42
42
|
inlineContent: Map<String, InlineTextContent> = mapOf(),
|
|
43
|
+
userScaleRate: Float? = null,
|
|
43
44
|
) {
|
|
44
45
|
Text(
|
|
45
46
|
text = AnnotatedString(text),
|
|
@@ -56,7 +57,8 @@ fun Text(
|
|
|
56
57
|
letterSpacing = letterSpacing,
|
|
57
58
|
softWrap = softWrap,
|
|
58
59
|
accessibilityId = accessibilityId,
|
|
59
|
-
inlineContent = inlineContent
|
|
60
|
+
inlineContent = inlineContent,
|
|
61
|
+
userScaleRate = userScaleRate
|
|
60
62
|
)
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -78,13 +80,14 @@ fun Text(
|
|
|
78
80
|
softWrap: Boolean = true,
|
|
79
81
|
accessibilityId: String? = null,
|
|
80
82
|
inlineContent: Map<String, InlineTextContent> = mapOf(),
|
|
83
|
+
userScaleRate: Float? = null,
|
|
81
84
|
) {
|
|
82
85
|
// Cache theme access to avoid repeated lookups
|
|
83
86
|
val theme = AppTheme.current
|
|
84
87
|
|
|
85
88
|
// Call @Composable functions directly in composable context
|
|
86
|
-
val scaledFontSize = scaleSize(style.fontSize)
|
|
87
|
-
val scaledLineHeight = scaleSize(style.lineHeight)
|
|
89
|
+
val scaledFontSize = scaleSize(style.fontSize, userScaleRate)
|
|
90
|
+
val scaledLineHeight = scaleSize(style.lineHeight, userScaleRate)
|
|
88
91
|
val fontFamilyResult = getFontFamily(fontFamily ?: theme.font, style.fontWeight)
|
|
89
92
|
|
|
90
93
|
// Now memoize the results
|
|
@@ -15,8 +15,7 @@ import androidx.compose.ui.unit.sp
|
|
|
15
15
|
import org.jetbrains.compose.resources.Font
|
|
16
16
|
import org.jetbrains.compose.resources.FontResource
|
|
17
17
|
import org.jetbrains.compose.resources.InternalResourceApi
|
|
18
|
-
import vn.momo.kits.application.
|
|
19
|
-
import vn.momo.kits.application.LocalUseDeviceFontScale
|
|
18
|
+
import vn.momo.kits.application.LocalFontScaleConfig
|
|
20
19
|
import vn.momo.kits.platform.getScreenDimensions
|
|
21
20
|
import vn.momo.uikits.resources.Res
|
|
22
21
|
import vn.momo.uikits.resources.momosignature
|
|
@@ -38,64 +37,67 @@ const val MAX_FONT_SCALE = 1.5f
|
|
|
38
37
|
const val MAX_DEVICE_SCALE = 5
|
|
39
38
|
|
|
40
39
|
@Composable
|
|
41
|
-
fun scaleSize(size: Float): Float {
|
|
42
|
-
|
|
43
|
-
val
|
|
44
|
-
val
|
|
45
|
-
|
|
40
|
+
fun scaleSize(size: Float, userScaleRate: Float? = null): Float {
|
|
41
|
+
// single source of truth: useOSFontScale + userScaleRate both read from LocalFontScaleConfig
|
|
42
|
+
val fontScaleConfig = LocalFontScaleConfig.current
|
|
43
|
+
val useOSFontScale = fontScaleConfig.useOSFontScale
|
|
44
|
+
// param userScaleRate overrides config; if absent -> config.userScaleRate -> 1f (no scaling)
|
|
45
|
+
val customRate = userScaleRate ?: fontScaleConfig.userScaleRate ?: 1f
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
val
|
|
47
|
+
// Read composables unconditionally (rule of Compose) before branching on the math below.
|
|
48
|
+
val deviceScale = getScreenDimensions().width / DEFAULT_SCREEN_SIZE
|
|
49
|
+
// OS font scale: prefer the host-pushed value (live on resume/config change, since the activity
|
|
50
|
+
// isn't recreated on fontScale change); fall back to the Compose density when not reported yet.
|
|
51
|
+
val fontScale = fontScaleConfig.osFontScale ?: LocalDensity.current.fontScale
|
|
52
|
+
|
|
53
|
+
// useOSFontScale off -> the user has full control via customRate; skip device-width scaling
|
|
54
|
+
// entirely and apply only the custom rate (capped at MAX_FONT_SCALE).
|
|
55
|
+
if (!useOSFontScale) {
|
|
56
|
+
// custom rate is uncapped (full user control); only the OS-follow path below caps at MAX_FONT_SCALE.
|
|
57
|
+
return if (customRate > 1f) customRate * size else size
|
|
58
|
+
}
|
|
49
59
|
|
|
60
|
+
// useOSFontScale on -> device-width scale + OS font scale, take the larger.
|
|
50
61
|
var fontSizeScaleDevice = size
|
|
51
62
|
var fontSizeScaleOS = size
|
|
52
|
-
|
|
53
63
|
if (deviceScale > 1) {
|
|
54
64
|
fontSizeScaleDevice =
|
|
55
65
|
min(deviceScale * fontSizeScaleDevice, fontSizeScaleDevice + MAX_DEVICE_SCALE)
|
|
56
66
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (fontScale > 1) {
|
|
60
|
-
fontSizeScaleOS = min(fontScale * fontSizeScaleOS, fontSizeScaleOS * MAX_FONT_SCALE)
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
fontSizeScaleOS = size * appFontScale
|
|
67
|
+
if (fontScale > 1f) {
|
|
68
|
+
fontSizeScaleOS = min(fontScale * fontSizeScaleOS, fontSizeScaleOS * MAX_FONT_SCALE)
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
return max(
|
|
67
|
-
fontSizeScaleDevice,
|
|
68
|
-
fontSizeScaleOS
|
|
69
|
-
)
|
|
71
|
+
return max(fontSizeScaleDevice, fontSizeScaleOS)
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
@Composable
|
|
73
|
-
fun scaleSize(size: TextUnit): TextUnit {
|
|
75
|
+
fun scaleSize(size: TextUnit, userScaleRate: Float? = null): TextUnit {
|
|
74
76
|
if (!size.isSp) return size
|
|
75
77
|
|
|
76
78
|
val density = LocalDensity.current
|
|
77
79
|
|
|
78
|
-
val scaled = scaleSize(size.value)
|
|
80
|
+
val scaled = scaleSize(size.value, userScaleRate)
|
|
79
81
|
val spValue = scaled / density.fontScale
|
|
80
82
|
|
|
81
83
|
return TextUnit(value = spValue, type = TextUnitType.Sp)
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
@Composable
|
|
85
|
-
fun scaleSize(size: Dp): Dp {
|
|
86
|
-
return scaleSize(size.value).dp
|
|
87
|
+
fun scaleSize(size: Dp, userScaleRate: Float? = null): Dp {
|
|
88
|
+
return scaleSize(size.value, userScaleRate).dp
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
@Composable
|
|
90
|
-
fun scaleSize(textStyle: TextStyle): TextStyle {
|
|
92
|
+
fun scaleSize(textStyle: TextStyle, userScaleRate: Float? = null): TextStyle {
|
|
91
93
|
return textStyle.copy(
|
|
92
94
|
fontSize = if (textStyle.fontSize != TextUnit.Unspecified)
|
|
93
|
-
scaleSize(textStyle.fontSize)
|
|
95
|
+
scaleSize(textStyle.fontSize, userScaleRate)
|
|
94
96
|
else
|
|
95
97
|
TextUnit.Unspecified,
|
|
96
98
|
|
|
97
99
|
lineHeight = if (textStyle.lineHeight != TextUnit.Unspecified)
|
|
98
|
-
scaleSize(textStyle.lineHeight)
|
|
100
|
+
scaleSize(textStyle.lineHeight, userScaleRate)
|
|
99
101
|
else
|
|
100
102
|
TextUnit.Unspecified,
|
|
101
103
|
)
|
|
@@ -9,6 +9,7 @@ import androidx.navigation.compose.NavHost
|
|
|
9
9
|
import androidx.navigation.compose.composable
|
|
10
10
|
import androidx.navigation.compose.rememberNavController
|
|
11
11
|
import androidx.navigation.toRoute
|
|
12
|
+
import kotlinx.coroutines.flow.collect
|
|
12
13
|
import vn.momo.kits.application.*
|
|
13
14
|
import vn.momo.kits.const.*
|
|
14
15
|
import vn.momo.kits.platform.ProvideNavigationEventDispatcherOwner
|
|
@@ -37,6 +38,19 @@ fun NavigationContainer(
|
|
|
37
38
|
val parentContext = ApplicationContext.current
|
|
38
39
|
val mergedContext = MiniAppContext.merge(parentContext, applicationContext)
|
|
39
40
|
|
|
41
|
+
// Default = the host-seeded fontScaleConfig from the merged context, so the first frame is
|
|
42
|
+
// already scaled (no flash). The observer below overwrites it on later host writes (live rate
|
|
43
|
+
// changes); a null emit (host cleared) falls back to the ambient LocalFontScaleConfig default.
|
|
44
|
+
var observerFontScaleConfig by remember { mutableStateOf(mergedContext?.fontScaleConfig) }
|
|
45
|
+
LaunchedEffect(maxApi) {
|
|
46
|
+
val api = maxApi ?: return@LaunchedEffect
|
|
47
|
+
runCatching {
|
|
48
|
+
api.observer(FONT_SCALE_OBSERVER_KEY).collect { data ->
|
|
49
|
+
observerFontScaleConfig = parseFontScaleConfig(data)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
val theme = remember { mutableStateOf(initialTheme) }
|
|
41
55
|
|
|
42
56
|
LaunchedEffect(Unit) {
|
|
@@ -64,8 +78,7 @@ fun NavigationContainer(
|
|
|
64
78
|
ApplicationContext provides mergedContext,
|
|
65
79
|
AppConfig provides config,
|
|
66
80
|
AppLanguage provides language,
|
|
67
|
-
|
|
68
|
-
LocalAppFontScale provides (mergedContext?.appFontScale ?: 1f),
|
|
81
|
+
LocalFontScaleConfig provides (observerFontScaleConfig ?: LocalFontScaleConfig.current),
|
|
69
82
|
) {
|
|
70
83
|
LaunchedEffect(Unit) {
|
|
71
84
|
setNavigator?.invoke(navigator)
|
|
@@ -144,3 +157,33 @@ fun NavigationContainer(
|
|
|
144
157
|
val LocalMaxApi = staticCompositionLocalOf<IMaxApi?> {
|
|
145
158
|
error("No MaxApi provided")
|
|
146
159
|
}
|
|
160
|
+
|
|
161
|
+
// Observer storage key the host app writes FontScaleConfig to. Lowercased natively.
|
|
162
|
+
private const val FONT_SCALE_OBSERVER_KEY = "font_scale_config"
|
|
163
|
+
|
|
164
|
+
// Host returns object {useOSFontScale, userScaleRate}; guard against envelope wrapping / type mismatch from marshaling.
|
|
165
|
+
private fun parseFontScaleConfig(raw: Map<String, Any?>?): FontScaleConfig? {
|
|
166
|
+
if (raw.isNullOrEmpty()) return null
|
|
167
|
+
@Suppress("UNCHECKED_CAST")
|
|
168
|
+
val data = (raw["response"] as? Map<String, Any?>)
|
|
169
|
+
?: (raw["data"] as? Map<String, Any?>)
|
|
170
|
+
?: raw
|
|
171
|
+
if (!data.containsKey("useOSFontScale") && !data.containsKey("userScaleRate")) return null
|
|
172
|
+
val useOS = when (val v = data["useOSFontScale"]) {
|
|
173
|
+
is Boolean -> v
|
|
174
|
+
is String -> v.equals("true", ignoreCase = true)
|
|
175
|
+
is Number -> v.toInt() != 0
|
|
176
|
+
else -> true
|
|
177
|
+
}
|
|
178
|
+
val rate = when (val v = data["userScaleRate"]) {
|
|
179
|
+
is Number -> v.toFloat()
|
|
180
|
+
is String -> v.toFloatOrNull()
|
|
181
|
+
else -> null
|
|
182
|
+
}
|
|
183
|
+
val osScale = when (val v = data["osFontScale"]) {
|
|
184
|
+
is Number -> v.toFloat()
|
|
185
|
+
is String -> v.toFloatOrNull()
|
|
186
|
+
else -> null
|
|
187
|
+
}
|
|
188
|
+
return FontScaleConfig(useOSFontScale = useOS, userScaleRate = rate, osFontScale = osScale)
|
|
189
|
+
}
|
package/gradle.properties
CHANGED
|
@@ -46,5 +46,7 @@ public class ApplicationEnvironment: ObservableObject {
|
|
|
46
46
|
self.applicationContext = applicationContext
|
|
47
47
|
self.composeApi = composeApi
|
|
48
48
|
self.config = config
|
|
49
|
+
// Load + observe the host font-scale config into the global store read by scaleSize/appFont.
|
|
50
|
+
FontScaleStore.shared.bind(composeApi)
|
|
49
51
|
}
|
|
50
52
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//
|
|
2
|
+
// FontScaleStore.swift
|
|
3
|
+
// Momo Design System (SwiftUI)
|
|
4
|
+
//
|
|
5
|
+
// Global font-scale config read by scaleSize/appFont. SwiftUI counterpart of the
|
|
6
|
+
// Compose LocalFontScaleConfig and the RN ScaleSizeContext. Populated from the host's
|
|
7
|
+
// `font_scale_config` observer-storage key via the KitComposeApi bridge.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import SwiftUI
|
|
11
|
+
|
|
12
|
+
public struct FontScaleConfig {
|
|
13
|
+
public var useOSFontScale: Bool
|
|
14
|
+
public var userScaleRate: CGFloat?
|
|
15
|
+
|
|
16
|
+
public init(useOSFontScale: Bool = true, userScaleRate: CGFloat? = nil) {
|
|
17
|
+
self.useOSFontScale = useOSFontScale
|
|
18
|
+
self.userScaleRate = userScaleRate
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Single source of truth for the font scale. `scaleSize`/`appFont` read `shared.config`.
|
|
23
|
+
/// Views that want live updates observe this object (e.g. MomoText).
|
|
24
|
+
public final class FontScaleStore: ObservableObject {
|
|
25
|
+
public static let shared = FontScaleStore()
|
|
26
|
+
|
|
27
|
+
@Published public private(set) var config = FontScaleConfig()
|
|
28
|
+
|
|
29
|
+
private var didBind = false
|
|
30
|
+
private var observerId: String?
|
|
31
|
+
|
|
32
|
+
private init() {}
|
|
33
|
+
|
|
34
|
+
public func update(_ newConfig: FontScaleConfig) {
|
|
35
|
+
if Thread.isMainThread {
|
|
36
|
+
config = newConfig
|
|
37
|
+
} else {
|
|
38
|
+
DispatchQueue.main.async { [weak self] in self?.config = newConfig }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Load the current config + subscribe to live changes through the host MaxApi bridge.
|
|
43
|
+
/// Safe no-op when no bridge is provided (config stays at default = no scaling).
|
|
44
|
+
public func bind(_ api: KitComposeApi?) {
|
|
45
|
+
guard let api = api, !didBind else { return }
|
|
46
|
+
didBind = true
|
|
47
|
+
|
|
48
|
+
// Synchronous snapshot — flash-free first frame. The compose bridge runs the platform API
|
|
49
|
+
// synchronously (getFontScaleConfig's storage flow is take(1), so it completes inline and
|
|
50
|
+
// returns the current value before the first render reads scaleSize/appFont).
|
|
51
|
+
apply(api.request(funcName: "getFontScaleConfig", params: nil))
|
|
52
|
+
// live: re-read on every host write to the key
|
|
53
|
+
observerId = api.request(funcName: "observer", params: ["font_scale_config"]) { [weak self] response in
|
|
54
|
+
self?.apply(response)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private func apply(_ response: String) {
|
|
59
|
+
guard let parsed = FontScaleStore.parse(response) else { return }
|
|
60
|
+
update(parsed)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Tolerates the MaxApi envelope {"status":..,"response":{..}} as well as a bare config object.
|
|
64
|
+
static func parse(_ response: String) -> FontScaleConfig? {
|
|
65
|
+
guard
|
|
66
|
+
let data = response.data(using: .utf8),
|
|
67
|
+
let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
|
|
68
|
+
else { return nil }
|
|
69
|
+
|
|
70
|
+
let obj = (json["response"] as? [String: Any]) ?? (json["data"] as? [String: Any]) ?? json
|
|
71
|
+
guard obj["useOSFontScale"] != nil || obj["userScaleRate"] != nil else { return nil }
|
|
72
|
+
|
|
73
|
+
let useOS = (obj["useOSFontScale"] as? Bool) ?? true
|
|
74
|
+
let rate = (obj["userScaleRate"] as? NSNumber).map { CGFloat($0.doubleValue) }
|
|
75
|
+
return FontScaleConfig(useOSFontScale: useOS, userScaleRate: rate)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
|
|
3
3
|
public func scaleSize(_ size: CGFloat, _ scaleRate: CGFloat? = nil) -> CGFloat {
|
|
4
|
+
// Single source of truth: useOSFontScale + userScaleRate from the host config.
|
|
5
|
+
// `scaleRate` param keeps its existing meaning: a hard cap override (default 1.5).
|
|
6
|
+
let config = FontScaleStore.shared.config
|
|
4
7
|
let defaultScreenSize: CGFloat = 375
|
|
5
8
|
let maxFontScale: CGFloat = scaleRate ?? 1.5
|
|
6
9
|
let maxDeviceScale: CGFloat = 5
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
// useOSFontScale off -> the user has full control via userScaleRate; skip device-width scaling
|
|
12
|
+
// entirely and apply only the custom rate (capped at maxFontScale). Mirrors Compose / RN.
|
|
13
|
+
if !config.useOSFontScale {
|
|
14
|
+
// custom rate is uncapped (full user control); only the OS-follow path below caps at maxFontScale.
|
|
15
|
+
let customRate = config.userScaleRate ?? 1
|
|
16
|
+
return customRate > 1 ? size * customRate : size
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// useOSFontScale on -> device-width scale + OS font scale, take the larger.
|
|
20
|
+
let deviceScale = UIScreen.main.bounds.width / defaultScreenSize
|
|
10
21
|
|
|
11
22
|
let defaultFont = UIFont.systemFont(ofSize: UIFont.labelFontSize)
|
|
12
23
|
let scaledFont = UIFontMetrics.default.scaledFont(for: defaultFont)
|
|
13
|
-
let
|
|
24
|
+
let osFontScale = scaledFont.pointSize / defaultFont.pointSize
|
|
14
25
|
|
|
15
26
|
var fontSizeDeviceScale = size
|
|
16
27
|
var fontSizeOSScale = size
|
|
17
28
|
|
|
18
|
-
|
|
19
29
|
if deviceScale > 1 {
|
|
20
30
|
fontSizeDeviceScale = min(fontSizeDeviceScale * deviceScale, fontSizeDeviceScale + maxDeviceScale)
|
|
21
31
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
fontSizeOSScale = min(fontSizeOSScale * fontScale, fontSizeOSScale * maxFontScale)
|
|
32
|
+
if osFontScale > 1 {
|
|
33
|
+
fontSizeOSScale = min(fontSizeOSScale * osFontScale, fontSizeOSScale * maxFontScale)
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
return max(fontSizeDeviceScale, fontSizeOSScale)
|
|
@@ -118,6 +127,8 @@ public struct MomoText: View {
|
|
|
118
127
|
private let content: String
|
|
119
128
|
private let typography: TypographyStyle
|
|
120
129
|
private let color: Color
|
|
130
|
+
// Observe the global store so the text re-renders live when the host changes the config.
|
|
131
|
+
@ObservedObject private var fontScale = FontScaleStore.shared
|
|
121
132
|
|
|
122
133
|
public init(
|
|
123
134
|
_ content: String,
|
|
@@ -130,6 +141,7 @@ public struct MomoText: View {
|
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
public var body: some View {
|
|
144
|
+
_ = fontScale.config
|
|
133
145
|
let text = SwiftUI.Text(content)
|
|
134
146
|
.font(.system(size: scaleSize(typography.fontSize), weight: typography.fontWeight))
|
|
135
147
|
.foregroundColor(color)
|
|
@@ -2,25 +2,38 @@ import SwiftUI
|
|
|
2
2
|
|
|
3
3
|
public extension Font {
|
|
4
4
|
static func appFont(size: CGFloat) -> Font {
|
|
5
|
+
// Single source of truth: useOSFontScale + userScaleRate from the host config.
|
|
6
|
+
// NOTE: the static Font.xxx constants below are evaluated once at type load, so they
|
|
7
|
+
// capture the config at first access. Live updates apply to scaleSize()/MomoText and
|
|
8
|
+
// any fresh appFont(size:) call, not to the cached static constants.
|
|
9
|
+
let config = FontScaleStore.shared.config
|
|
5
10
|
let defaultScreenSize: CGFloat = 375
|
|
6
11
|
let maxFontScale: CGFloat = 1.5
|
|
7
12
|
let maxDeviceScale: CGFloat = 5
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
|
|
14
|
+
// useOSFontScale off -> the user has full control via userScaleRate; skip device-width
|
|
15
|
+
// scaling entirely and apply only the custom rate (capped at maxFontScale).
|
|
16
|
+
if !config.useOSFontScale {
|
|
17
|
+
// custom rate is uncapped (full user control); only the OS-follow path below caps at maxFontScale.
|
|
18
|
+
let customRate = config.userScaleRate ?? 1
|
|
19
|
+
let scaled = customRate > 1 ? size * customRate : size
|
|
20
|
+
return Font.system(size: scaled)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// useOSFontScale on -> device-width scale, then OS font scale on top (existing behavior).
|
|
24
|
+
let deviceScale = UIScreen.main.bounds.width / defaultScreenSize
|
|
25
|
+
|
|
12
26
|
let defaultFont = UIFont.systemFont(ofSize: UIFont.labelFontSize)
|
|
13
27
|
let scaledFont = UIFontMetrics.default.scaledFont(for: defaultFont)
|
|
14
|
-
let
|
|
28
|
+
let osFontScale = scaledFont.pointSize / defaultFont.pointSize
|
|
15
29
|
|
|
16
30
|
var fontSize = size
|
|
17
31
|
|
|
18
32
|
if deviceScale > 1 {
|
|
19
33
|
fontSize = min(fontSize * deviceScale, fontSize + maxDeviceScale)
|
|
20
34
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
fontSize = min(fontSize * fontScale, fontSize * maxFontScale)
|
|
35
|
+
if osFontScale > 1 {
|
|
36
|
+
fontSize = min(fontSize * osFontScale, fontSize * maxFontScale)
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
return Font.system(size: fontSize)
|