react-native-tinyui 0.4.0 → 1.0.0

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.
Files changed (157) hide show
  1. package/README.md +383 -53
  2. package/ios/ConcentricView/TinyuiConcentricView.h +12 -0
  3. package/ios/ConcentricView/TinyuiConcentricView.mm +60 -0
  4. package/ios/Core/Tinyui.mm +54 -0
  5. package/ios/Core/TinyuiComponentClasses.mm +44 -0
  6. package/ios/LiquidGlassText/TinyuiLiquidGlassTextProvider.swift +64 -79
  7. package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.h +46 -0
  8. package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.mm +159 -0
  9. package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.mm +6 -27
  10. package/ios/LiquidGlassText/TinyuiTextOutlineShape.swift +5 -2
  11. package/ios/Menu/TinyuiMenuBuilder.swift +238 -0
  12. package/ios/Menu/TinyuiMenuProvider.swift +51 -85
  13. package/ios/Menu/TinyuiMenuView.mm +13 -35
  14. package/ios/Popover/TinyuiPopoverProvider.swift +198 -44
  15. package/ios/Popover/TinyuiPopoverView.mm +26 -1
  16. package/ios/SFSymbol/TinyuiSFSymbolEffects.h +9 -0
  17. package/ios/SFSymbol/TinyuiSFSymbolEffects.mm +180 -0
  18. package/ios/SFSymbol/TinyuiSFSymbolImage.h +8 -0
  19. package/ios/SFSymbol/TinyuiSFSymbolImage.mm +141 -0
  20. package/ios/SFSymbol/TinyuiSFSymbolShadowNode.h +31 -0
  21. package/ios/SFSymbol/TinyuiSFSymbolShadowNode.mm +30 -0
  22. package/ios/SFSymbol/TinyuiSFSymbolView.h +4 -0
  23. package/ios/SFSymbol/TinyuiSFSymbolView.mm +173 -0
  24. package/ios/Stepper/TinyuiStepperShadowNode.h +32 -0
  25. package/ios/Stepper/TinyuiStepperShadowNode.mm +28 -0
  26. package/ios/Stepper/TinyuiStepperView.h +12 -0
  27. package/ios/Stepper/TinyuiStepperView.mm +128 -0
  28. package/ios/TipKit/TinyuiTip.swift +31 -0
  29. package/ios/TipKit/TinyuiTipPopoverProvider.swift +276 -0
  30. package/ios/TipKit/TinyuiTipPopoverView.h +8 -0
  31. package/ios/TipKit/TinyuiTipPopoverView.mm +114 -0
  32. package/ios/TipKit/TinyuiTipsRuntime.swift +148 -0
  33. package/lib/module/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
  34. package/lib/module/ConcentricView/index.js +29 -0
  35. package/lib/module/ConcentricView/index.js.map +1 -0
  36. package/lib/module/ConcentricView/types.js +4 -0
  37. package/lib/module/ConcentricView/types.js.map +1 -0
  38. package/lib/module/LiquidGlassText/LiquidGlassTextNativeComponent.ts +2 -12
  39. package/lib/module/LiquidGlassText/index.js +60 -35
  40. package/lib/module/LiquidGlassText/index.js.map +1 -1
  41. package/lib/module/Menu/MenuNativeComponent.ts +7 -5
  42. package/lib/module/Menu/index.js +4 -9
  43. package/lib/module/Menu/index.js.map +1 -1
  44. package/lib/module/Menu/options.js +4 -12
  45. package/lib/module/Menu/options.js.map +1 -1
  46. package/lib/module/NativeTinyui.js.map +1 -1
  47. package/lib/module/Popover/index.js +4 -2
  48. package/lib/module/Popover/index.js.map +1 -1
  49. package/lib/module/SFSymbol/SFSymbolNativeComponent.ts +20 -0
  50. package/lib/module/SFSymbol/index.js +73 -0
  51. package/lib/module/SFSymbol/index.js.map +1 -0
  52. package/lib/module/SFSymbol/options.js +79 -0
  53. package/lib/module/SFSymbol/options.js.map +1 -0
  54. package/lib/module/SFSymbol/types.js +4 -0
  55. package/lib/module/SFSymbol/types.js.map +1 -0
  56. package/lib/module/Stepper/StepperNativeComponent.ts +29 -0
  57. package/lib/module/Stepper/index.js +75 -0
  58. package/lib/module/Stepper/index.js.map +1 -0
  59. package/lib/module/Stepper/types.js +4 -0
  60. package/lib/module/Stepper/types.js.map +1 -0
  61. package/lib/module/TipKit/Popover.js +89 -0
  62. package/lib/module/TipKit/Popover.js.map +1 -0
  63. package/lib/module/TipKit/TipPopoverNativeComponent.ts +37 -0
  64. package/lib/module/TipKit/index.js +28 -0
  65. package/lib/module/TipKit/index.js.map +1 -0
  66. package/lib/module/TipKit/types.js +4 -0
  67. package/lib/module/TipKit/types.js.map +1 -0
  68. package/lib/module/TipKit/validation.js +24 -0
  69. package/lib/module/TipKit/validation.js.map +1 -0
  70. package/lib/module/index.js +4 -0
  71. package/lib/module/index.js.map +1 -1
  72. package/lib/module/utils/getNativeTinyui.js.map +1 -1
  73. package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts +7 -0
  74. package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts.map +1 -0
  75. package/lib/typescript/src/ConcentricView/index.d.ts +4 -0
  76. package/lib/typescript/src/ConcentricView/index.d.ts.map +1 -0
  77. package/lib/typescript/src/ConcentricView/types.d.ts +6 -0
  78. package/lib/typescript/src/ConcentricView/types.d.ts.map +1 -0
  79. package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts +2 -8
  80. package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts.map +1 -1
  81. package/lib/typescript/src/LiquidGlassText/index.d.ts +1 -1
  82. package/lib/typescript/src/LiquidGlassText/index.d.ts.map +1 -1
  83. package/lib/typescript/src/LiquidGlassText/types.d.ts +13 -22
  84. package/lib/typescript/src/LiquidGlassText/types.d.ts.map +1 -1
  85. package/lib/typescript/src/Menu/MenuNativeComponent.d.ts +4 -5
  86. package/lib/typescript/src/Menu/MenuNativeComponent.d.ts.map +1 -1
  87. package/lib/typescript/src/Menu/index.d.ts +1 -1
  88. package/lib/typescript/src/Menu/index.d.ts.map +1 -1
  89. package/lib/typescript/src/Menu/options.d.ts +40 -4
  90. package/lib/typescript/src/Menu/options.d.ts.map +1 -1
  91. package/lib/typescript/src/Menu/types.d.ts +16 -4
  92. package/lib/typescript/src/Menu/types.d.ts.map +1 -1
  93. package/lib/typescript/src/NativeTinyui.d.ts +2 -0
  94. package/lib/typescript/src/NativeTinyui.d.ts.map +1 -1
  95. package/lib/typescript/src/Popover/index.d.ts.map +1 -1
  96. package/lib/typescript/src/Popover/types.d.ts +2 -0
  97. package/lib/typescript/src/Popover/types.d.ts.map +1 -1
  98. package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts +11 -0
  99. package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts.map +1 -0
  100. package/lib/typescript/src/SFSymbol/index.d.ts +4 -0
  101. package/lib/typescript/src/SFSymbol/index.d.ts.map +1 -0
  102. package/lib/typescript/src/SFSymbol/options.d.ts +86 -0
  103. package/lib/typescript/src/SFSymbol/options.d.ts.map +1 -0
  104. package/lib/typescript/src/SFSymbol/types.d.ts +113 -0
  105. package/lib/typescript/src/SFSymbol/types.d.ts.map +1 -0
  106. package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts +21 -0
  107. package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts.map +1 -0
  108. package/lib/typescript/src/Stepper/index.d.ts +4 -0
  109. package/lib/typescript/src/Stepper/index.d.ts.map +1 -0
  110. package/lib/typescript/src/Stepper/types.d.ts +24 -0
  111. package/lib/typescript/src/Stepper/types.d.ts.map +1 -0
  112. package/lib/typescript/src/TipKit/Popover.d.ts +3 -0
  113. package/lib/typescript/src/TipKit/Popover.d.ts.map +1 -0
  114. package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts +38 -0
  115. package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts.map +1 -0
  116. package/lib/typescript/src/TipKit/index.d.ts +10 -0
  117. package/lib/typescript/src/TipKit/index.d.ts.map +1 -0
  118. package/lib/typescript/src/TipKit/types.d.ts +46 -0
  119. package/lib/typescript/src/TipKit/types.d.ts.map +1 -0
  120. package/lib/typescript/src/TipKit/validation.d.ts +4 -0
  121. package/lib/typescript/src/TipKit/validation.d.ts.map +1 -0
  122. package/lib/typescript/src/index.d.ts +10 -2
  123. package/lib/typescript/src/index.d.ts.map +1 -1
  124. package/lib/typescript/src/utils/getNativeTinyui.d.ts +1 -1
  125. package/lib/typescript/src/utils/getNativeTinyui.d.ts.map +1 -1
  126. package/package.json +37 -2
  127. package/{Tinyui.podspec → react-native-tinyui.podspec} +23 -2
  128. package/src/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
  129. package/src/ConcentricView/index.tsx +34 -0
  130. package/src/ConcentricView/types.ts +6 -0
  131. package/src/LiquidGlassText/LiquidGlassTextNativeComponent.ts +2 -12
  132. package/src/LiquidGlassText/index.tsx +73 -39
  133. package/src/LiquidGlassText/types.ts +24 -50
  134. package/src/Menu/MenuNativeComponent.ts +7 -5
  135. package/src/Menu/index.tsx +6 -6
  136. package/src/Menu/options.ts +4 -18
  137. package/src/Menu/types.ts +18 -4
  138. package/src/NativeTinyui.ts +2 -0
  139. package/src/Popover/index.tsx +8 -3
  140. package/src/Popover/types.ts +2 -0
  141. package/src/SFSymbol/SFSymbolNativeComponent.ts +20 -0
  142. package/src/SFSymbol/index.tsx +89 -0
  143. package/src/SFSymbol/options.ts +116 -0
  144. package/src/SFSymbol/types.ts +158 -0
  145. package/src/Stepper/StepperNativeComponent.ts +29 -0
  146. package/src/Stepper/index.tsx +91 -0
  147. package/src/Stepper/types.ts +24 -0
  148. package/src/TipKit/Popover.tsx +87 -0
  149. package/src/TipKit/TipPopoverNativeComponent.ts +37 -0
  150. package/src/TipKit/index.ts +47 -0
  151. package/src/TipKit/types.ts +57 -0
  152. package/src/TipKit/validation.ts +39 -0
  153. package/src/index.tsx +32 -4
  154. package/src/utils/getNativeTinyui.ts +8 -1
  155. package/ios/Core/TinyuiHostingHelpers.swift +0 -74
  156. package/ios/Menu/TinyuiMenuViewSwift.swift +0 -408
  157. package/ios/Popover/TinyuiPopoverViewSwift.swift +0 -88
@@ -1,4 +1,7 @@
1
1
  #import "Tinyui.h"
2
+ #if TINYUI_FEATURE_TIP_KIT
3
+ #import "Tinyui-Swift.h"
4
+ #endif
2
5
 
3
6
  @implementation Tinyui
4
7
 
@@ -16,10 +19,61 @@
16
19
  #if TINYUI_FEATURE_LIQUID_GLASS_TEXT
17
20
  [components addObject:@"LiquidGlassText"];
18
21
  #endif
22
+ #if TINYUI_FEATURE_STEPPER
23
+ [components addObject:@"Stepper"];
24
+ #endif
25
+ #if TINYUI_FEATURE_CONCENTRIC_VIEW
26
+ [components addObject:@"ConcentricView"];
27
+ #endif
28
+ #if TINYUI_FEATURE_SF_SYMBOL
29
+ [components addObject:@"SFSymbol"];
30
+ #endif
31
+ #if TINYUI_FEATURE_TIP_KIT
32
+ [components addObject:@"TipKit"];
33
+ #endif
19
34
 
20
35
  return components;
21
36
  }
22
37
 
38
+ - (void)configureTips:(NSString *)displayFrequency
39
+ resolve:(RCTPromiseResolveBlock)resolve
40
+ reject:(RCTPromiseRejectBlock)reject
41
+ {
42
+ #if TINYUI_FEATURE_TIP_KIT
43
+ dispatch_async(dispatch_get_main_queue(), ^{
44
+ [TinyuiTipsRuntime configure:displayFrequency completion:^(NSError *error) {
45
+ if (error != nil) {
46
+ reject(error.userInfo[@"code"] ?: @"E_TIPS_CONFIGURATION", error.localizedDescription, error);
47
+ } else {
48
+ resolve(nil);
49
+ }
50
+ }];
51
+ });
52
+ #else
53
+ reject(@"E_TIP_NOT_INSTALLED", @"Enable TipKit in react-native-tinyui.components and run pod install.", nil);
54
+ #endif
55
+ }
56
+
57
+ - (void)invalidateTip:(NSString *)tipId
58
+ reason:(NSString *)reason
59
+ resolve:(RCTPromiseResolveBlock)resolve
60
+ reject:(RCTPromiseRejectBlock)reject
61
+ {
62
+ #if TINYUI_FEATURE_TIP_KIT
63
+ dispatch_async(dispatch_get_main_queue(), ^{
64
+ [TinyuiTipsRuntime invalidate:tipId reason:reason completion:^(NSError *error) {
65
+ if (error != nil) {
66
+ reject(error.userInfo[@"code"] ?: @"E_TIP_INVALIDATION", error.localizedDescription, error);
67
+ } else {
68
+ resolve(nil);
69
+ }
70
+ }];
71
+ });
72
+ #else
73
+ reject(@"E_TIP_NOT_INSTALLED", @"Enable TipKit in react-native-tinyui.components and run pod install.", nil);
74
+ #endif
75
+ }
76
+
23
77
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
24
78
  (const facebook::react::ObjCTurboModule::InitParams &)params
25
79
  {
@@ -35,6 +35,30 @@ TINYUI_DISABLED_COMPONENT(TinyuiPopoverView, TinyuiPopoverViewComponentDescripto
35
35
  TINYUI_DISABLED_COMPONENT(TinyuiLiquidGlassTextView, TinyuiLiquidGlassTextViewComponentDescriptor)
36
36
  #endif
37
37
 
38
+ #if TINYUI_FEATURE_STEPPER
39
+ #import "TinyuiStepperView.h"
40
+ #else
41
+ TINYUI_DISABLED_COMPONENT(TinyuiStepperView, TinyuiStepperViewComponentDescriptor)
42
+ #endif
43
+
44
+ #if TINYUI_FEATURE_CONCENTRIC_VIEW
45
+ #import "TinyuiConcentricView.h"
46
+ #else
47
+ TINYUI_DISABLED_COMPONENT(TinyuiConcentricView, TinyuiConcentricViewComponentDescriptor)
48
+ #endif
49
+
50
+ #if TINYUI_FEATURE_SF_SYMBOL
51
+ #import "TinyuiSFSymbolView.h"
52
+ #else
53
+ TINYUI_DISABLED_COMPONENT(TinyuiSFSymbolView, TinyuiSFSymbolViewComponentDescriptor)
54
+ #endif
55
+
56
+ #if TINYUI_FEATURE_TIP_KIT
57
+ #import "TinyuiTipPopoverView.h"
58
+ #else
59
+ TINYUI_DISABLED_COMPONENT(TinyuiTipPopoverView, TinyuiTipPopoverViewComponentDescriptor)
60
+ #endif
61
+
38
62
  #undef TINYUI_DISABLED_COMPONENT
39
63
 
40
64
  Class<RCTComponentViewProtocol> TinyuiMenuViewCls(void)
@@ -51,3 +75,23 @@ Class<RCTComponentViewProtocol> TinyuiLiquidGlassTextViewCls(void)
51
75
  {
52
76
  return TinyuiLiquidGlassTextView.class;
53
77
  }
78
+
79
+ Class<RCTComponentViewProtocol> TinyuiStepperViewCls(void)
80
+ {
81
+ return TinyuiStepperView.class;
82
+ }
83
+
84
+ Class<RCTComponentViewProtocol> TinyuiConcentricViewCls(void)
85
+ {
86
+ return TinyuiConcentricView.class;
87
+ }
88
+
89
+ Class<RCTComponentViewProtocol> TinyuiSFSymbolViewCls(void)
90
+ {
91
+ return TinyuiSFSymbolView.class;
92
+ }
93
+
94
+ Class<RCTComponentViewProtocol> TinyuiTipPopoverViewCls(void)
95
+ {
96
+ return TinyuiTipPopoverView.class;
97
+ }
@@ -2,60 +2,64 @@ import CoreText
2
2
  import SwiftUI
3
3
  import UIKit
4
4
 
5
- private struct LiquidGlassTextConfiguration: Decodable {
6
- struct FontConfiguration: Decodable {
7
- let style: String?
8
- let size: CGFloat?
9
- let weight: String?
10
- let design: String?
11
- let family: String?
5
+ private struct LiquidGlassTextConfiguration {
6
+ struct TextStyleConfiguration {
7
+ let fontSize: CGFloat?
8
+ let fontFamily: String?
9
+ let fontWeight: String?
10
+ let fontStyle: String?
11
+ let letterSpacing: CGFloat?
12
+
13
+ init(_ dictionary: [String: Any]) {
14
+ fontSize = (dictionary["fontSize"] as? NSNumber).map { CGFloat($0.doubleValue) }
15
+ fontFamily = dictionary["fontFamily"] as? String
16
+ fontWeight = dictionary["fontWeight"] as? String
17
+ fontStyle = dictionary["fontStyle"] as? String
18
+ letterSpacing = (dictionary["letterSpacing"] as? NSNumber).map { CGFloat($0.doubleValue) }
19
+ }
12
20
  }
13
21
 
14
22
  var text: String?
15
- var glass: String?
23
+ var effect: String?
16
24
  var interactive: Bool?
17
- var font: FontConfiguration?
18
- var fontWeight: String?
19
25
  var fontDesign: String?
26
+ var textStyle: TextStyleConfiguration?
20
27
  var multilineTextAlignment: String?
21
28
 
29
+ init(_ dictionary: [String: Any] = [:]) {
30
+ text = dictionary["text"] as? String
31
+ effect = dictionary["effect"] as? String
32
+ interactive = dictionary["interactive"] as? Bool
33
+ fontDesign = dictionary["fontDesign"] as? String
34
+ textStyle = (dictionary["textStyle"] as? [String: Any]).map(TextStyleConfiguration.init)
35
+ multilineTextAlignment = dictionary["multilineTextAlignment"] as? String
36
+ }
37
+
22
38
  var resolvedFont: Font {
23
39
  let design: Font.Design
24
- switch fontDesign ?? font?.design {
40
+ switch fontDesign {
25
41
  case "serif": design = .serif
26
42
  case "monospaced": design = .monospaced
27
43
  case "rounded": design = .rounded
28
44
  default: design = .default
29
45
  }
30
46
 
31
- let style: Font.TextStyle
32
- switch font?.style {
33
- case "largeTitle": style = .largeTitle
34
- case "title": style = .title
35
- case "title2": style = .title2
36
- case "title3": style = .title3
37
- case "headline": style = .headline
38
- case "subheadline": style = .subheadline
39
- case "callout": style = .callout
40
- case "footnote": style = .footnote
41
- case "caption": style = .caption
42
- case "caption2": style = .caption2
43
- default: style = .body
44
- }
45
-
47
+ let size = textStyle?.fontSize
48
+ let family = textStyle?.fontFamily
46
49
  var result: Font
47
- if let size = font?.size, size.isFinite, size > 0 {
48
- if let family = font?.family, !family.isEmpty {
50
+ if let family, !family.isEmpty {
51
+ if let size, size.isFinite, size > 0 {
49
52
  result = .custom(family, fixedSize: size)
50
53
  } else {
51
- result = .system(size: size, design: design)
54
+ result = .custom(family, size: 17, relativeTo: .body)
52
55
  }
56
+ } else if let size, size.isFinite, size > 0 {
57
+ result = .system(size: size, design: design)
53
58
  } else {
54
- result = .system(style, design: design)
59
+ result = .system(.body, design: design)
55
60
  }
56
61
 
57
- // Preserve a semantic style's default weight (e.g. headline) unless overridden.
58
- if let weight = fontWeight ?? font?.weight {
62
+ if let weight = textStyle?.fontWeight {
59
63
  switch weight {
60
64
  case "ultraLight": result = result.weight(.ultraLight)
61
65
  case "thin": result = result.weight(.thin)
@@ -68,6 +72,9 @@ private struct LiquidGlassTextConfiguration: Decodable {
68
72
  default: result = result.weight(.regular)
69
73
  }
70
74
  }
75
+ if textStyle?.fontStyle == "italic" {
76
+ result = result.italic()
77
+ }
71
78
  return result
72
79
  }
73
80
 
@@ -78,6 +85,11 @@ private struct LiquidGlassTextConfiguration: Decodable {
78
85
  default: return .leading
79
86
  }
80
87
  }
88
+
89
+ var resolvedLetterSpacing: CGFloat {
90
+ guard let letterSpacing = textStyle?.letterSpacing, letterSpacing.isFinite else { return 0 }
91
+ return letterSpacing
92
+ }
81
93
  }
82
94
 
83
95
  private struct LiquidGlassTextContent: View {
@@ -90,12 +102,14 @@ private struct LiquidGlassTextContent: View {
90
102
  if #available(iOS 26.0, *) {
91
103
  LiquidGlassTextEffect(
92
104
  text: text,
93
- effect: configuration.glass ?? "clear",
105
+ effect: configuration.effect ?? "clear",
94
106
  tintColor: tintColor,
95
- interactive: configuration.interactive ?? false
107
+ interactive: configuration.interactive ?? false,
108
+ letterSpacing: configuration.resolvedLetterSpacing
96
109
  )
97
110
  } else {
98
111
  Text(verbatim: text)
112
+ .tracking(configuration.resolvedLetterSpacing)
99
113
  .foregroundStyle(tintColor.map { Color(uiColor: $0) } ?? .primary)
100
114
  }
101
115
  }
@@ -114,6 +128,7 @@ private struct LiquidGlassTextEffect: View {
114
128
  let effect: String
115
129
  let tintColor: UIColor?
116
130
  let interactive: Bool
131
+ let letterSpacing: CGFloat
117
132
 
118
133
  @Environment(\.font) private var font
119
134
  @Environment(\.fontResolutionContext) private var fontResolutionContext
@@ -141,14 +156,21 @@ private struct LiquidGlassTextEffect: View {
141
156
  let width = lines.reduce(CGFloat.zero) { width, line in
142
157
  let attributed = NSAttributedString(
143
158
  string: line,
144
- attributes: [kCTFontAttributeName as NSAttributedString.Key: ctFont]
159
+ attributes: [
160
+ kCTFontAttributeName as NSAttributedString.Key: ctFont,
161
+ kCTKernAttributeName as NSAttributedString.Key: letterSpacing,
162
+ ]
145
163
  )
146
164
  return max(width, CGFloat(CTLineGetTypographicBounds(
147
165
  CTLineCreateWithAttributedString(attributed), nil, nil, nil
148
166
  )))
149
167
  }
150
168
  let shape = TinyuiTextOutlineShape(
151
- text: text, ctFont: ctFont, fontSize: resolved.pointSize, alignment: alignment
169
+ text: text,
170
+ ctFont: ctFont,
171
+ fontSize: resolved.pointSize,
172
+ alignment: alignment,
173
+ letterSpacing: letterSpacing
152
174
  )
153
175
  Rectangle()
154
176
  .fill(.clear)
@@ -157,44 +179,22 @@ private struct LiquidGlassTextEffect: View {
157
179
  }
158
180
  }
159
181
 
160
- @objc public protocol TinyuiLiquidGlassTextViewDelegate: AnyObject {
161
- func onContentSizeChange(width: Double, height: Double, configuration: String)
162
- }
163
-
164
182
  @objc public class TinyuiLiquidGlassTextProvider: UIView {
165
- private weak var delegate: TinyuiLiquidGlassTextViewDelegate?
166
183
  private var hostingController: UIHostingController<LiquidGlassTextContent>?
167
184
  private var configuration = LiquidGlassTextConfiguration()
168
- private var configurationJSON = "{}"
169
185
  private var tintColorValue: UIColor?
170
- private var lastSize: CGSize?
171
186
 
172
187
  @objc public weak var parentViewController: UIViewController? {
173
188
  didSet { setNeedsLayout() }
174
189
  }
175
190
 
176
- @objc public convenience init(delegate: TinyuiLiquidGlassTextViewDelegate) {
177
- self.init(frame: .zero)
178
- self.delegate = delegate
179
- registerForTraitChanges([UITraitPreferredContentSizeCategory.self]) {
180
- (view: TinyuiLiquidGlassTextProvider, _: UITraitCollection) in
181
- view.invalidateMeasurement()
182
- }
183
- }
184
-
185
- @objc public func configure(_ json: String, tintColor: UIColor?) {
186
- guard json != configurationJSON || tintColor != tintColorValue else { return }
187
- configurationJSON = json
188
- configuration = (try? JSONDecoder().decode(
189
- LiquidGlassTextConfiguration.self, from: Data(json.utf8)
190
- )) ?? LiquidGlassTextConfiguration()
191
+ @objc public func configure(
192
+ _ dictionary: [String: Any],
193
+ tintColor: UIColor?
194
+ ) {
195
+ configuration = LiquidGlassTextConfiguration(dictionary)
191
196
  tintColorValue = tintColor
192
197
  hostingController?.rootView = content
193
- invalidateMeasurement()
194
- }
195
-
196
- @objc public func invalidateMeasurement() {
197
- lastSize = nil
198
198
  setNeedsLayout()
199
199
  }
200
200
 
@@ -207,7 +207,7 @@ private struct LiquidGlassTextEffect: View {
207
207
  if window == nil {
208
208
  detachController()
209
209
  } else {
210
- invalidateMeasurement()
210
+ setNeedsLayout()
211
211
  }
212
212
  }
213
213
 
@@ -221,10 +221,8 @@ private struct LiquidGlassTextEffect: View {
221
221
  @objc public func reset() {
222
222
  detachController()
223
223
  parentViewController = nil
224
- configurationJSON = "{}"
225
224
  configuration = LiquidGlassTextConfiguration()
226
225
  tintColorValue = nil
227
- lastSize = nil
228
226
  }
229
227
 
230
228
  override public func layoutSubviews() {
@@ -248,18 +246,5 @@ private struct LiquidGlassTextEffect: View {
248
246
  controller.didMove(toParent: parentViewController)
249
247
  }
250
248
  controller.view.frame = bounds
251
-
252
- // GlassText uses explicit newlines and intrinsic sizing, never automatic wrapping.
253
- let measured = controller.sizeThatFits(in: CGSize(width: 100_000, height: 100_000))
254
- let scale = traitCollection.displayScale > 0 ? traitCollection.displayScale : 1
255
- let size = CGSize(
256
- width: ceil(measured.width * scale) / scale,
257
- height: ceil(measured.height * scale) / scale
258
- )
259
- guard size.width.isFinite, size.height.isFinite, size != lastSize else { return }
260
- lastSize = size
261
- delegate?.onContentSizeChange(
262
- width: size.width, height: size.height, configuration: configurationJSON
263
- )
264
249
  }
265
250
  }
@@ -0,0 +1,46 @@
1
+ #pragma once
2
+
3
+ #include <react/renderer/components/TinyuiSpec/ShadowNodes.h>
4
+ #include <react/renderer/components/view/ConcreteViewShadowNode.h>
5
+ #include <react/renderer/core/ConcreteComponentDescriptor.h>
6
+
7
+ namespace facebook::react {
8
+
9
+ class TinyuiLiquidGlassTextShadowNode final
10
+ : public ConcreteViewShadowNode<
11
+ TinyuiLiquidGlassTextViewComponentName,
12
+ TinyuiLiquidGlassTextViewProps,
13
+ TinyuiLiquidGlassTextViewEventEmitter,
14
+ TinyuiLiquidGlassTextViewState> {
15
+ public:
16
+ using BaseShadowNode = ConcreteViewShadowNode<
17
+ TinyuiLiquidGlassTextViewComponentName,
18
+ TinyuiLiquidGlassTextViewProps,
19
+ TinyuiLiquidGlassTextViewEventEmitter,
20
+ TinyuiLiquidGlassTextViewState>;
21
+ using BaseShadowNode::BaseShadowNode;
22
+
23
+ static ShadowNodeTraits BaseTraits()
24
+ {
25
+ auto traits = BaseShadowNode::BaseTraits();
26
+ traits.set(ShadowNodeTraits::Trait::LeafYogaNode);
27
+ traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode);
28
+ return traits;
29
+ }
30
+
31
+ Size measureContent(
32
+ const LayoutContext &layoutContext,
33
+ const LayoutConstraints &layoutConstraints) const override;
34
+ };
35
+
36
+ class TinyuiLiquidGlassTextComponentDescriptor final
37
+ : public ConcreteComponentDescriptor<TinyuiLiquidGlassTextShadowNode> {
38
+ public:
39
+ TinyuiLiquidGlassTextComponentDescriptor(
40
+ const ComponentDescriptorParameters &parameters)
41
+ : ConcreteComponentDescriptor(parameters)
42
+ {
43
+ }
44
+ };
45
+
46
+ } // namespace facebook::react
@@ -0,0 +1,159 @@
1
+ #import <CoreText/CoreText.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ #include <algorithm>
5
+ #include <cmath>
6
+ #include <string>
7
+
8
+ #include <react/renderer/core/LayoutConstraints.h>
9
+ #include <react/renderer/core/LayoutContext.h>
10
+
11
+ #include "TinyuiLiquidGlassTextShadowNode.h"
12
+
13
+ namespace facebook::react {
14
+
15
+ static std::string stringValue(
16
+ const folly::dynamic &object,
17
+ folly::StringPiece key,
18
+ const std::string &fallback = "")
19
+ {
20
+ if (!object.isObject()) {
21
+ return fallback;
22
+ }
23
+ auto value = object.getDefault(key, fallback);
24
+ return value.isString() ? value.getString() : fallback;
25
+ }
26
+
27
+ static CGFloat numberValue(
28
+ const folly::dynamic &object,
29
+ folly::StringPiece key,
30
+ CGFloat fallback)
31
+ {
32
+ if (!object.isObject()) {
33
+ return fallback;
34
+ }
35
+ auto value = object.getDefault(key, fallback);
36
+ return value.isNumber() ? static_cast<CGFloat>(value.asDouble()) : fallback;
37
+ }
38
+
39
+ static UIFontWeight fontWeight(const std::string &value)
40
+ {
41
+ if (value == "ultraLight") return UIFontWeightUltraLight;
42
+ if (value == "thin") return UIFontWeightThin;
43
+ if (value == "light") return UIFontWeightLight;
44
+ if (value == "medium") return UIFontWeightMedium;
45
+ if (value == "semibold") return UIFontWeightSemibold;
46
+ if (value == "bold") return UIFontWeightBold;
47
+ if (value == "heavy") return UIFontWeightHeavy;
48
+ if (value == "black") return UIFontWeightBlack;
49
+ return UIFontWeightRegular;
50
+ }
51
+
52
+ static UIFontDescriptorSystemDesign fontDesign(const std::string &value)
53
+ {
54
+ if (value == "serif") return UIFontDescriptorSystemDesignSerif;
55
+ if (value == "monospaced") return UIFontDescriptorSystemDesignMonospaced;
56
+ if (value == "rounded") return UIFontDescriptorSystemDesignRounded;
57
+ return nil;
58
+ }
59
+
60
+ static UIFont *resolvedFont(
61
+ const folly::dynamic &configuration,
62
+ const folly::dynamic &textStyle,
63
+ Float fontSizeMultiplier)
64
+ {
65
+ auto configuredSize = numberValue(textStyle, "fontSize", 0);
66
+ auto multiplier = std::isfinite(fontSizeMultiplier) && fontSizeMultiplier > 0
67
+ ? fontSizeMultiplier
68
+ : 1;
69
+ auto size = std::isfinite(configuredSize) && configuredSize > 0
70
+ ? configuredSize
71
+ : 17 * multiplier;
72
+ auto family = stringValue(textStyle, "fontFamily");
73
+ auto weight = stringValue(textStyle, "fontWeight");
74
+
75
+ UIFont *font = nil;
76
+ if (!family.empty()) {
77
+ NSString *familyName = [NSString stringWithUTF8String:family.c_str()];
78
+ font = familyName == nil ? nil : [UIFont fontWithName:familyName size:size];
79
+ }
80
+ if (font == nil) {
81
+ font = [UIFont systemFontOfSize:size weight:fontWeight(weight)];
82
+ }
83
+
84
+ UIFontDescriptor *descriptor = font.fontDescriptor;
85
+ if (family.empty()) {
86
+ auto design = fontDesign(stringValue(configuration, "fontDesign"));
87
+ if (design != nil) {
88
+ descriptor = [descriptor fontDescriptorWithDesign:design] ?: descriptor;
89
+ }
90
+ }
91
+
92
+ if (!weight.empty() && !family.empty()) {
93
+ descriptor = [descriptor fontDescriptorByAddingAttributes:@{
94
+ UIFontDescriptorTraitsAttribute: @{UIFontWeightTrait: @(fontWeight(weight))}
95
+ }];
96
+ }
97
+ if (stringValue(textStyle, "fontStyle") == "italic") {
98
+ descriptor = [descriptor fontDescriptorWithSymbolicTraits:
99
+ descriptor.symbolicTraits | UIFontDescriptorTraitItalic] ?: descriptor;
100
+ }
101
+ return [UIFont fontWithDescriptor:descriptor size:size];
102
+ }
103
+
104
+ Size TinyuiLiquidGlassTextShadowNode::measureContent(
105
+ const LayoutContext &layoutContext,
106
+ const LayoutConstraints &layoutConstraints) const
107
+ {
108
+ @autoreleasepool {
109
+ const auto &configuration = getConcreteProps().configuration;
110
+ if (!configuration.isObject()) {
111
+ return layoutConstraints.clamp({});
112
+ }
113
+
114
+ auto textValue = stringValue(configuration, "text");
115
+ if (textValue.empty()) {
116
+ return layoutConstraints.clamp({});
117
+ }
118
+ NSString *text = [NSString stringWithUTF8String:textValue.c_str()] ?: @"";
119
+ auto textStyle = configuration.getDefault("textStyle", folly::dynamic::object);
120
+ UIFont *font = resolvedFont(configuration, textStyle, layoutContext.fontSizeMultiplier);
121
+ auto letterSpacing = numberValue(textStyle, "letterSpacing", 0);
122
+ if (!std::isfinite(letterSpacing)) {
123
+ letterSpacing = 0;
124
+ }
125
+
126
+ CTFontRef ctFont = CTFontCreateWithName(
127
+ (__bridge CFStringRef)font.fontName,
128
+ font.pointSize,
129
+ nullptr);
130
+ auto lineHeight = CTFontGetAscent(ctFont) + CTFontGetDescent(ctFont) +
131
+ std::max(CTFontGetLeading(ctFont), CTFontGetSize(ctFont) * 0.2);
132
+ CGFloat width = 0;
133
+ auto lines = [text componentsSeparatedByCharactersInSet:NSCharacterSet.newlineCharacterSet];
134
+ NSDictionary<NSAttributedStringKey, id> *attributes = @{
135
+ (__bridge NSAttributedStringKey)kCTFontAttributeName: (__bridge id)ctFont,
136
+ (__bridge NSAttributedStringKey)kCTKernAttributeName: @(letterSpacing),
137
+ };
138
+ for (NSString *line in lines) {
139
+ auto attributed = [[NSAttributedString alloc] initWithString:line attributes:attributes];
140
+ CTLineRef ctLine = CTLineCreateWithAttributedString(
141
+ (__bridge CFAttributedStringRef)attributed);
142
+ width = std::max(
143
+ width,
144
+ static_cast<CGFloat>(CTLineGetTypographicBounds(ctLine, nullptr, nullptr, nullptr)));
145
+ CFRelease(ctLine);
146
+ }
147
+ CFRelease(ctFont);
148
+
149
+ auto height = lineHeight * lines.count;
150
+ auto scale = layoutContext.pointScaleFactor > 0 ? layoutContext.pointScaleFactor : 1;
151
+ Size measured{
152
+ .width = static_cast<Float>(std::ceil(width * scale) / scale),
153
+ .height = static_cast<Float>(std::ceil(height * scale) / scale),
154
+ };
155
+ return layoutConstraints.clamp(measured);
156
+ }
157
+ }
158
+
159
+ } // namespace facebook::react
@@ -4,17 +4,16 @@
4
4
 
5
5
  #import <React/RCTConversions.h>
6
6
  #import <React/UIView+React.h>
7
- #import <react/renderer/components/TinyuiSpec/ComponentDescriptors.h>
8
- #import <react/renderer/components/TinyuiSpec/EventEmitters.h>
7
+ #import <react/utils/FollyConvert.h>
9
8
  #import <react/renderer/components/TinyuiSpec/Props.h>
10
9
  #import <react/renderer/components/TinyuiSpec/RCTComponentViewHelpers.h>
11
10
  #import "RCTFabricComponentsPlugins.h"
12
11
  #import "Tinyui-Swift.h"
12
+ #import "TinyuiLiquidGlassTextShadowNode.h"
13
13
 
14
14
  using namespace facebook::react;
15
15
 
16
- @interface TinyuiLiquidGlassTextView () <RCTTinyuiLiquidGlassTextViewViewProtocol,
17
- TinyuiLiquidGlassTextViewDelegate>
16
+ @interface TinyuiLiquidGlassTextView () <RCTTinyuiLiquidGlassTextViewViewProtocol>
18
17
  @end
19
18
 
20
19
  @implementation TinyuiLiquidGlassTextView {
@@ -23,7 +22,7 @@ using namespace facebook::react;
23
22
 
24
23
  + (ComponentDescriptorProvider)componentDescriptorProvider
25
24
  {
26
- return concreteComponentDescriptorProvider<TinyuiLiquidGlassTextViewComponentDescriptor>();
25
+ return concreteComponentDescriptorProvider<TinyuiLiquidGlassTextComponentDescriptor>();
27
26
  }
28
27
 
29
28
  - (instancetype)initWithFrame:(CGRect)frame
@@ -31,7 +30,7 @@ using namespace facebook::react;
31
30
  if (self = [super initWithFrame:frame]) {
32
31
  static const auto defaultProps = std::make_shared<const TinyuiLiquidGlassTextViewProps>();
33
32
  _props = defaultProps;
34
- _provider = [[TinyuiLiquidGlassTextProvider alloc] initWithDelegate:self];
33
+ _provider = [[TinyuiLiquidGlassTextProvider alloc] initWithFrame:CGRectZero];
35
34
  self.contentView = _provider;
36
35
  }
37
36
  return self;
@@ -48,31 +47,11 @@ using namespace facebook::react;
48
47
  - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
49
48
  {
50
49
  const auto &newProps = *std::static_pointer_cast<TinyuiLiquidGlassTextViewProps const>(props);
51
- [_provider configure:RCTNSStringFromString(newProps.configuration)
50
+ [_provider configure:convertFollyDynamicToId(newProps.configuration)
52
51
  tintColor:RCTUIColorFromSharedColor(newProps.tintColor)];
53
52
  [super updateProps:props oldProps:oldProps];
54
53
  }
55
54
 
56
- - (void)updateEventEmitter:(EventEmitter::Shared const &)eventEmitter
57
- {
58
- [super updateEventEmitter:eventEmitter];
59
- [_provider invalidateMeasurement];
60
- }
61
-
62
- - (void)onContentSizeChangeWithWidth:(double)width
63
- height:(double)height
64
- configuration:(NSString *)configuration
65
- {
66
- if (_eventEmitter == nullptr) {
67
- return;
68
- }
69
- std::static_pointer_cast<const TinyuiLiquidGlassTextViewEventEmitter>(_eventEmitter)
70
- ->onContentSizeChange(TinyuiLiquidGlassTextViewEventEmitter::OnContentSizeChange{
71
- .width = width,
72
- .height = height,
73
- .configuration = RCTStringFromNSString(configuration)});
74
- }
75
-
76
55
  - (void)prepareForRecycle
77
56
  {
78
57
  [_provider reset];
@@ -16,6 +16,7 @@ struct TinyuiTextOutlineShape: Shape {
16
16
  var ctFont: CTFont
17
17
  var fontSize: CGFloat
18
18
  var alignment: TextAlignment = .center
19
+ var letterSpacing: CGFloat = 0
19
20
 
20
21
  func path(in rect: CGRect) -> Path {
21
22
  // Split into explicit lines (no wrapping here)
@@ -25,9 +26,11 @@ struct TinyuiTextOutlineShape: Shape {
25
26
  var ctLines: [CTLine] = []
26
27
  ctLines.reserveCapacity(rawLines.count)
27
28
 
28
- let attrKey = kCTFontAttributeName as NSAttributedString.Key
29
29
  for l in rawLines {
30
- let attr = [attrKey: ctFont]
30
+ let attr: [NSAttributedString.Key: Any] = [
31
+ kCTFontAttributeName as NSAttributedString.Key: ctFont,
32
+ kCTKernAttributeName as NSAttributedString.Key: letterSpacing,
33
+ ]
31
34
  let astr = NSAttributedString(string: l, attributes: attr)
32
35
  let line = CTLineCreateWithAttributedString(astr)
33
36
  ctLines.append(line)