react-native-tinyui 0.3.0 → 0.5.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 (103) hide show
  1. package/README.md +70 -2
  2. package/Tinyui.podspec +7 -1
  3. package/ios/Core/Tinyui.mm +4 -0
  4. package/ios/Core/TinyuiComponentClasses.mm +35 -9
  5. package/ios/LiquidGlassText/GlassText-LICENSE +21 -0
  6. package/ios/LiquidGlassText/TinyuiLiquidGlassTextProvider.swift +250 -0
  7. package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.h +46 -0
  8. package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.mm +159 -0
  9. package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.h +12 -0
  10. package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.mm +63 -0
  11. package/ios/LiquidGlassText/TinyuiTextOutlineShape.swift +116 -0
  12. package/ios/Menu/TinyuiMenuProvider.swift +2 -2
  13. package/ios/Menu/TinyuiMenuView.mm +2 -1
  14. package/ios/Menu/TinyuiMenuViewSwift.swift +42 -124
  15. package/lib/module/LiquidGlassText/LiquidGlassTextNativeComponent.ts +16 -0
  16. package/lib/module/LiquidGlassText/index.js +87 -0
  17. package/lib/module/LiquidGlassText/index.js.map +1 -0
  18. package/lib/module/LiquidGlassText/types.js +4 -0
  19. package/lib/module/LiquidGlassText/types.js.map +1 -0
  20. package/lib/module/{MenuNativeComponent.ts → Menu/MenuNativeComponent.ts} +6 -2
  21. package/lib/module/{Menu.js → Menu/index.js} +3 -3
  22. package/lib/module/Menu/index.js.map +1 -0
  23. package/lib/module/{MenuOptions.js → Menu/options.js} +3 -3
  24. package/lib/module/Menu/options.js.map +1 -0
  25. package/lib/module/Menu/types.js +4 -0
  26. package/lib/module/Menu/types.js.map +1 -0
  27. package/lib/module/{PopoverPrimitives.js → Popover/PopoverClose.js} +1 -1
  28. package/lib/module/Popover/PopoverClose.js.map +1 -0
  29. package/lib/module/{Popover.js → Popover/index.js} +3 -3
  30. package/lib/module/Popover/index.js.map +1 -0
  31. package/lib/module/Popover/types.js +4 -0
  32. package/lib/module/Popover/types.js.map +1 -0
  33. package/lib/module/index.js +4 -3
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/module/{getNativeTinyui.js → utils/getNativeTinyui.js} +1 -1
  36. package/lib/module/utils/getNativeTinyui.js.map +1 -0
  37. package/lib/module/utils/index.js +4 -0
  38. package/lib/module/utils/index.js.map +1 -0
  39. package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts +8 -0
  40. package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts.map +1 -0
  41. package/lib/typescript/src/LiquidGlassText/index.d.ts +4 -0
  42. package/lib/typescript/src/LiquidGlassText/index.d.ts.map +1 -0
  43. package/lib/typescript/src/LiquidGlassText/types.d.ts +22 -0
  44. package/lib/typescript/src/LiquidGlassText/types.d.ts.map +1 -0
  45. package/lib/typescript/src/{MenuNativeComponent.d.ts → Menu/MenuNativeComponent.d.ts} +2 -2
  46. package/lib/typescript/src/Menu/MenuNativeComponent.d.ts.map +1 -0
  47. package/lib/typescript/src/{Menu.d.ts → Menu/index.d.ts} +3 -3
  48. package/lib/typescript/src/Menu/index.d.ts.map +1 -0
  49. package/lib/typescript/src/Menu/options.d.ts +47 -0
  50. package/lib/typescript/src/Menu/options.d.ts.map +1 -0
  51. package/lib/typescript/src/{Menu.types.d.ts → Menu/types.d.ts} +1 -1
  52. package/lib/typescript/src/Menu/types.d.ts.map +1 -0
  53. package/lib/typescript/src/{PopoverPrimitives.d.ts → Popover/PopoverClose.d.ts} +2 -2
  54. package/lib/typescript/src/Popover/PopoverClose.d.ts.map +1 -0
  55. package/lib/typescript/src/Popover/PopoverNativeComponent.d.ts.map +1 -0
  56. package/lib/typescript/src/Popover/index.d.ts +8 -0
  57. package/lib/typescript/src/Popover/index.d.ts.map +1 -0
  58. package/lib/typescript/src/{Popover.types.d.ts → Popover/types.d.ts} +1 -1
  59. package/lib/typescript/src/Popover/types.d.ts.map +1 -0
  60. package/lib/typescript/src/index.d.ts +8 -6
  61. package/lib/typescript/src/index.d.ts.map +1 -1
  62. package/lib/typescript/src/{getNativeTinyui.d.ts → utils/getNativeTinyui.d.ts} +2 -2
  63. package/lib/typescript/src/utils/getNativeTinyui.d.ts.map +1 -0
  64. package/lib/typescript/src/utils/index.d.ts +3 -0
  65. package/lib/typescript/src/utils/index.d.ts.map +1 -0
  66. package/package.json +19 -10
  67. package/src/LiquidGlassText/LiquidGlassTextNativeComponent.ts +16 -0
  68. package/src/LiquidGlassText/index.tsx +113 -0
  69. package/src/LiquidGlassText/types.ts +37 -0
  70. package/src/{MenuNativeComponent.ts → Menu/MenuNativeComponent.ts} +6 -2
  71. package/src/{Menu.tsx → Menu/index.tsx} +4 -4
  72. package/src/{MenuOptions.ts → Menu/options.ts} +7 -3
  73. package/src/{PopoverPrimitives.tsx → Popover/PopoverClose.tsx} +1 -1
  74. package/src/{Popover.tsx → Popover/index.tsx} +12 -3
  75. package/src/index.tsx +12 -4
  76. package/src/react-native.d.ts +1 -0
  77. package/src/{getNativeTinyui.ts → utils/getNativeTinyui.ts} +2 -2
  78. package/src/utils/index.ts +2 -0
  79. package/lib/module/Menu.js.map +0 -1
  80. package/lib/module/Menu.types.js +0 -4
  81. package/lib/module/Menu.types.js.map +0 -1
  82. package/lib/module/MenuOptions.js.map +0 -1
  83. package/lib/module/Popover.js.map +0 -1
  84. package/lib/module/Popover.types.js +0 -4
  85. package/lib/module/Popover.types.js.map +0 -1
  86. package/lib/module/PopoverPrimitives.js.map +0 -1
  87. package/lib/module/getNativeTinyui.js.map +0 -1
  88. package/lib/typescript/src/Menu.d.ts.map +0 -1
  89. package/lib/typescript/src/Menu.types.d.ts.map +0 -1
  90. package/lib/typescript/src/MenuNativeComponent.d.ts.map +0 -1
  91. package/lib/typescript/src/MenuOptions.d.ts +0 -7
  92. package/lib/typescript/src/MenuOptions.d.ts.map +0 -1
  93. package/lib/typescript/src/Popover.d.ts +0 -7
  94. package/lib/typescript/src/Popover.d.ts.map +0 -1
  95. package/lib/typescript/src/Popover.types.d.ts.map +0 -1
  96. package/lib/typescript/src/PopoverNativeComponent.d.ts.map +0 -1
  97. package/lib/typescript/src/PopoverPrimitives.d.ts.map +0 -1
  98. package/lib/typescript/src/getNativeTinyui.d.ts.map +0 -1
  99. /package/lib/module/{PopoverNativeComponent.ts → Popover/PopoverNativeComponent.ts} +0 -0
  100. /package/lib/typescript/src/{PopoverNativeComponent.d.ts → Popover/PopoverNativeComponent.d.ts} +0 -0
  101. /package/src/{Menu.types.ts → Menu/types.ts} +0 -0
  102. /package/src/{PopoverNativeComponent.ts → Popover/PopoverNativeComponent.ts} +0 -0
  103. /package/src/{Popover.types.ts → Popover/types.ts} +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # react-native-tinyui
2
2
 
3
- Dependency-free, native iOS `Menu` and `Popover` components for React Native's
3
+ Dependency-free, native iOS `Menu`, `Popover` and `LiquidGlassText` components for React Native's
4
4
  New Architecture. The public API follows normal React composition patterns;
5
5
  the native UI is implemented with SwiftUI behind Fabric components.
6
6
 
@@ -44,8 +44,9 @@ Available components:
44
44
 
45
45
  - `Menu`
46
46
  - `Popover`
47
+ - `LiquidGlassText`
47
48
 
48
- Both components are enabled by default. Use an empty `components` array to
49
+ All components are enabled by default. Use an empty `components` array to
49
50
  compile only the TurboModule core. Run `pod install` again whenever this list
50
51
  changes. Rendering a component whose native part was not selected throws an
51
52
  error that names the missing component.
@@ -55,6 +56,7 @@ Component-specific JavaScript entry points are also available:
55
56
  ```tsx
56
57
  import { Menu } from 'react-native-tinyui/menu';
57
58
  import { Popover } from 'react-native-tinyui/popover';
59
+ import { LiquidGlassText } from 'react-native-tinyui/liquid-glass-text';
58
60
  ```
59
61
 
60
62
  The root `react-native-tinyui` imports remain supported for backwards
@@ -162,6 +164,72 @@ Use `open` with `onOpenChange` for a controlled popover, or `defaultOpen` for an
162
164
  uncontrolled one. `contentSize` defaults to 320 × 240 points and can be updated
163
165
  while the popover is visible.
164
166
 
167
+ ## LiquidGlassText
168
+
169
+ `LiquidGlassText` renders native glass inside the glyph outlines, based on
170
+ [GlassText](https://github.com/ailtonvivaz/GlassText). It exposes the effect and
171
+ typography through React props.
172
+ Background image loading is not part of this component.
173
+
174
+ Building this component requires **Xcode 26+**. Glass is rendered on **iOS 26+**;
175
+ on iOS 17–25 it displays ordinary SwiftUI text using the same font, alignment
176
+ and optional tint. The rest of TinyUI continues to support iOS 17+.
177
+
178
+ ```tsx
179
+ import { LiquidGlassText } from 'react-native-tinyui';
180
+
181
+ <LiquidGlassText
182
+ text="Liquid Glass"
183
+ effect="regular"
184
+ tint="#00BBDD"
185
+ interactive
186
+ fontDesign="rounded"
187
+ textStyle={{ fontSize: 48, fontWeight: '700' }}
188
+ />;
189
+
190
+ <LiquidGlassText
191
+ text={'Multi-line\nGlass Text\nEffect'}
192
+ fontDesign="serif"
193
+ textStyle={{ fontSize: 34, fontWeight: '800' }}
194
+ multilineTextAlignment="center"
195
+ />;
196
+ ```
197
+
198
+ | Prop | Values | Default |
199
+ | ------------------------ | ------------------------------------------------ | --------- |
200
+ | `text` | String (including text translated in JavaScript) | Required |
201
+ | `effect` | `clear`, `regular`, `identity` | `clear` |
202
+ | `tint` | React Native `ColorValue` | — |
203
+ | `interactive` | Whether the glass responds to interaction | `false` |
204
+ | `fontDesign` | `default`, `serif`, `monospaced`, `rounded` | `default` |
205
+ | `textStyle` | Supported React Native text properties | — |
206
+ | `multilineTextAlignment` | `leading`, `center`, `trailing` | `leading` |
207
+
208
+ `textStyle` accepts arrays and `StyleSheet.create` references. Explicit
209
+ `multilineTextAlignment` overrides `textStyle.textAlign`. `textStyle.color`
210
+ supplies the glass tint and fallback text color; an explicit `tint` takes
211
+ precedence. `fontFamily` accepts the PostScript name of a font installed in the
212
+ app, and `fontDesign` only applies to system fonts.
213
+
214
+ `tint` accepts a React Native `ColorValue`, including `PlatformColor` and
215
+ `DynamicColorIOS`. `identity` applies no glass effect.
216
+ The component also accepts standard `ViewProps`, including `style`, `testID`,
217
+ accessibility props and a native view ref. The supplied text is used for
218
+ the default accessibility label; pass `accessibilityLabel` to override it.
219
+
220
+ Fabric measures the native text during layout and supplies its default width and
221
+ height without a JavaScript measurement pass. Layout styles can override these
222
+ dimensions, but do not resize the font or wrap text. As in GlassText, use
223
+ explicit `\n` characters for multiple lines. Keep padding on a surrounding
224
+ `View`. Color emoji and other glyphs without vector outlines cannot produce a
225
+ glass shape.
226
+
227
+ `text` accepts a string. Applications using JavaScript localization can pass
228
+ their translated string directly.
229
+
230
+ The adapted Core Text outline implementation retains the upstream MIT notice in
231
+ [`ios/LiquidGlassText/GlassText-LICENSE`](ios/LiquidGlassText/GlassText-LICENSE).
232
+
165
233
  ## Contributing
166
234
 
167
235
  - [Development workflow](CONTRIBUTING.md#development-workflow)
package/Tinyui.podspec CHANGED
@@ -14,6 +14,11 @@ components = {
14
14
  :definition => "TINYUI_FEATURE_POPOVER=1",
15
15
  :frameworks => ["UIKit"],
16
16
  },
17
+ "LiquidGlassText" => {
18
+ :source_files => "ios/LiquidGlassText/**/*.{h,m,mm,swift,cpp}",
19
+ :definition => "TINYUI_FEATURE_LIQUID_GLASS_TEXT=1",
20
+ :frameworks => ["UIKit", "SwiftUI", "CoreText"],
21
+ },
17
22
  }
18
23
 
19
24
  configured_components = nil
@@ -74,7 +79,8 @@ Pod::Spec.new do |s|
74
79
  s.source = { :git => "https://github.com/luoxuhai/react-native-tinyui.git", :tag => "#{s.version}" }
75
80
 
76
81
  s.source_files = source_files
77
- s.private_header_files = ["ios/Core/**/*.h", "ios/Menu/**/*.h", "ios/Popover/**/*.h"]
82
+ s.private_header_files = ["ios/Core/**/*.h", "ios/Menu/**/*.h", "ios/Popover/**/*.h", "ios/LiquidGlassText/**/*.h"]
83
+ s.preserve_paths = "ios/LiquidGlassText/GlassText-LICENSE"
78
84
  s.swift_version = "5.0"
79
85
  s.frameworks = frameworks unless frameworks.empty?
80
86
  s.pod_target_xcconfig = {
@@ -13,6 +13,10 @@
13
13
  [components addObject:@"Popover"];
14
14
  #endif
15
15
 
16
+ #if TINYUI_FEATURE_LIQUID_GLASS_TEXT
17
+ [components addObject:@"LiquidGlassText"];
18
+ #endif
19
+
16
20
  return components;
17
21
  }
18
22
 
@@ -1,27 +1,53 @@
1
- #import <React/RCTComponentViewProtocol.h>
1
+ #import <React/RCTViewComponentView.h>
2
+ #import <react/renderer/components/TinyuiSpec/ComponentDescriptors.h>
3
+
4
+ using namespace facebook::react;
5
+
6
+ // Newer Codegen providers put every declared component into an NSDictionary
7
+ // using NSClassFromString, which cannot be nil. Keep a lightweight Fabric class
8
+ // for disabled components; the JS availability check prevents rendering them.
9
+ // Their actual UIKit/SwiftUI implementations remain excluded by the podspec.
10
+ #define TINYUI_DISABLED_COMPONENT(ViewClass, DescriptorClass) \
11
+ @interface ViewClass : RCTViewComponentView \
12
+ @end \
13
+ @implementation ViewClass \
14
+ + (ComponentDescriptorProvider)componentDescriptorProvider \
15
+ { \
16
+ return concreteComponentDescriptorProvider<DescriptorClass>(); \
17
+ } \
18
+ @end
2
19
 
3
20
  #if TINYUI_FEATURE_MENU
4
21
  #import "TinyuiMenuView.h"
22
+ #else
23
+ TINYUI_DISABLED_COMPONENT(TinyuiMenuView, TinyuiMenuViewComponentDescriptor)
5
24
  #endif
6
25
 
7
26
  #if TINYUI_FEATURE_POPOVER
8
27
  #import "TinyuiPopoverView.h"
28
+ #else
29
+ TINYUI_DISABLED_COMPONENT(TinyuiPopoverView, TinyuiPopoverViewComponentDescriptor)
9
30
  #endif
10
31
 
32
+ #if TINYUI_FEATURE_LIQUID_GLASS_TEXT
33
+ #import "TinyuiLiquidGlassTextView.h"
34
+ #else
35
+ TINYUI_DISABLED_COMPONENT(TinyuiLiquidGlassTextView, TinyuiLiquidGlassTextViewComponentDescriptor)
36
+ #endif
37
+
38
+ #undef TINYUI_DISABLED_COMPONENT
39
+
11
40
  Class<RCTComponentViewProtocol> TinyuiMenuViewCls(void)
12
41
  {
13
- #if TINYUI_FEATURE_MENU
14
42
  return TinyuiMenuView.class;
15
- #else
16
- return nil;
17
- #endif
18
43
  }
19
44
 
20
45
  Class<RCTComponentViewProtocol> TinyuiPopoverViewCls(void)
21
46
  {
22
- #if TINYUI_FEATURE_POPOVER
23
47
  return TinyuiPopoverView.class;
24
- #else
25
- return nil;
26
- #endif
48
+ }
49
+
50
+ Class<RCTComponentViewProtocol> TinyuiLiquidGlassTextViewCls(void)
51
+ {
52
+ return TinyuiLiquidGlassTextView.class;
27
53
  }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ailton Vieira
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,250 @@
1
+ import CoreText
2
+ import SwiftUI
3
+ import UIKit
4
+
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
+ }
20
+ }
21
+
22
+ var text: String?
23
+ var effect: String?
24
+ var interactive: Bool?
25
+ var fontDesign: String?
26
+ var textStyle: TextStyleConfiguration?
27
+ var multilineTextAlignment: String?
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
+
38
+ var resolvedFont: Font {
39
+ let design: Font.Design
40
+ switch fontDesign {
41
+ case "serif": design = .serif
42
+ case "monospaced": design = .monospaced
43
+ case "rounded": design = .rounded
44
+ default: design = .default
45
+ }
46
+
47
+ let size = textStyle?.fontSize
48
+ let family = textStyle?.fontFamily
49
+ var result: Font
50
+ if let family, !family.isEmpty {
51
+ if let size, size.isFinite, size > 0 {
52
+ result = .custom(family, fixedSize: size)
53
+ } else {
54
+ result = .custom(family, size: 17, relativeTo: .body)
55
+ }
56
+ } else if let size, size.isFinite, size > 0 {
57
+ result = .system(size: size, design: design)
58
+ } else {
59
+ result = .system(.body, design: design)
60
+ }
61
+
62
+ if let weight = textStyle?.fontWeight {
63
+ switch weight {
64
+ case "ultraLight": result = result.weight(.ultraLight)
65
+ case "thin": result = result.weight(.thin)
66
+ case "light": result = result.weight(.light)
67
+ case "medium": result = result.weight(.medium)
68
+ case "semibold": result = result.weight(.semibold)
69
+ case "bold": result = result.weight(.bold)
70
+ case "heavy": result = result.weight(.heavy)
71
+ case "black": result = result.weight(.black)
72
+ default: result = result.weight(.regular)
73
+ }
74
+ }
75
+ if textStyle?.fontStyle == "italic" {
76
+ result = result.italic()
77
+ }
78
+ return result
79
+ }
80
+
81
+ var alignment: TextAlignment {
82
+ switch multilineTextAlignment {
83
+ case "center": return .center
84
+ case "trailing": return .trailing
85
+ default: return .leading
86
+ }
87
+ }
88
+
89
+ var resolvedLetterSpacing: CGFloat {
90
+ guard let letterSpacing = textStyle?.letterSpacing, letterSpacing.isFinite else { return 0 }
91
+ return letterSpacing
92
+ }
93
+ }
94
+
95
+ private struct LiquidGlassTextContent: View {
96
+ let configuration: LiquidGlassTextConfiguration
97
+ let text: String
98
+ let tintColor: UIColor?
99
+
100
+ var body: some View {
101
+ Group {
102
+ if #available(iOS 26.0, *) {
103
+ LiquidGlassTextEffect(
104
+ text: text,
105
+ effect: configuration.effect ?? "clear",
106
+ tintColor: tintColor,
107
+ interactive: configuration.interactive ?? false,
108
+ letterSpacing: configuration.resolvedLetterSpacing
109
+ )
110
+ } else {
111
+ Text(verbatim: text)
112
+ .tracking(configuration.resolvedLetterSpacing)
113
+ .foregroundStyle(tintColor.map { Color(uiColor: $0) } ?? .primary)
114
+ }
115
+ }
116
+ .font(configuration.resolvedFont)
117
+ .multilineTextAlignment(configuration.alignment)
118
+ .fixedSize()
119
+ // React Native exposes one accessible text element with the supplied label.
120
+ .accessibilityHidden(true)
121
+ }
122
+ }
123
+
124
+ // Adapted from ailtonvivaz/GlassText (MIT). See GlassText-LICENSE.
125
+ @available(iOS 26.0, *)
126
+ private struct LiquidGlassTextEffect: View {
127
+ let text: String
128
+ let effect: String
129
+ let tintColor: UIColor?
130
+ let interactive: Bool
131
+ let letterSpacing: CGFloat
132
+
133
+ @Environment(\.font) private var font
134
+ @Environment(\.fontResolutionContext) private var fontResolutionContext
135
+ @Environment(\.multilineTextAlignment) private var alignment
136
+
137
+ private var glass: Glass {
138
+ var result: Glass
139
+ switch effect {
140
+ case "regular": result = .regular
141
+ case "identity": result = .identity
142
+ default: result = .clear
143
+ }
144
+ if let tintColor {
145
+ result = result.tint(Color(uiColor: tintColor))
146
+ }
147
+ return result.interactive(interactive)
148
+ }
149
+
150
+ var body: some View {
151
+ let resolved = (font ?? .body).resolve(in: fontResolutionContext)
152
+ let ctFont = resolved.ctFont
153
+ let lines = text.components(separatedBy: CharacterSet.newlines)
154
+ let lineHeight = CTFontGetAscent(ctFont) + CTFontGetDescent(ctFont)
155
+ + max(CTFontGetLeading(ctFont), resolved.pointSize * 0.2)
156
+ let width = lines.reduce(CGFloat.zero) { width, line in
157
+ let attributed = NSAttributedString(
158
+ string: line,
159
+ attributes: [
160
+ kCTFontAttributeName as NSAttributedString.Key: ctFont,
161
+ kCTKernAttributeName as NSAttributedString.Key: letterSpacing,
162
+ ]
163
+ )
164
+ return max(width, CGFloat(CTLineGetTypographicBounds(
165
+ CTLineCreateWithAttributedString(attributed), nil, nil, nil
166
+ )))
167
+ }
168
+ let shape = TinyuiTextOutlineShape(
169
+ text: text,
170
+ ctFont: ctFont,
171
+ fontSize: resolved.pointSize,
172
+ alignment: alignment,
173
+ letterSpacing: letterSpacing
174
+ )
175
+ Rectangle()
176
+ .fill(.clear)
177
+ .glassEffect(glass, in: shape)
178
+ .frame(width: width, height: text.isEmpty ? 0 : lineHeight * CGFloat(lines.count))
179
+ }
180
+ }
181
+
182
+ @objc public class TinyuiLiquidGlassTextProvider: UIView {
183
+ private var hostingController: UIHostingController<LiquidGlassTextContent>?
184
+ private var configuration = LiquidGlassTextConfiguration()
185
+ private var tintColorValue: UIColor?
186
+
187
+ @objc public weak var parentViewController: UIViewController? {
188
+ didSet { setNeedsLayout() }
189
+ }
190
+
191
+ @objc public func configure(
192
+ _ dictionary: [String: Any],
193
+ tintColor: UIColor?
194
+ ) {
195
+ configuration = LiquidGlassTextConfiguration(dictionary)
196
+ tintColorValue = tintColor
197
+ hostingController?.rootView = content
198
+ setNeedsLayout()
199
+ }
200
+
201
+ private var content: LiquidGlassTextContent {
202
+ LiquidGlassTextContent(configuration: configuration, text: configuration.text ?? "", tintColor: tintColorValue)
203
+ }
204
+
205
+ override public func didMoveToWindow() {
206
+ super.didMoveToWindow()
207
+ if window == nil {
208
+ detachController()
209
+ } else {
210
+ setNeedsLayout()
211
+ }
212
+ }
213
+
214
+ private func detachController() {
215
+ hostingController?.willMove(toParent: nil)
216
+ hostingController?.view.removeFromSuperview()
217
+ hostingController?.removeFromParent()
218
+ hostingController = nil
219
+ }
220
+
221
+ @objc public func reset() {
222
+ detachController()
223
+ parentViewController = nil
224
+ configuration = LiquidGlassTextConfiguration()
225
+ tintColorValue = nil
226
+ }
227
+
228
+ override public func layoutSubviews() {
229
+ super.layoutSubviews()
230
+ guard window != nil, let parentViewController else { return }
231
+
232
+ if hostingController == nil {
233
+ let controller = UIHostingController(rootView: content)
234
+ controller.safeAreaRegions = []
235
+ controller.view.backgroundColor = .clear
236
+ parentViewController.addChild(controller)
237
+ addSubview(controller.view)
238
+ controller.didMove(toParent: parentViewController)
239
+ hostingController = controller
240
+ }
241
+ guard let controller = hostingController else { return }
242
+ if controller.parent !== parentViewController {
243
+ controller.willMove(toParent: nil)
244
+ controller.removeFromParent()
245
+ parentViewController.addChild(controller)
246
+ controller.didMove(toParent: parentViewController)
247
+ }
248
+ controller.view.frame = bounds
249
+ }
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
@@ -0,0 +1,12 @@
1
+ #if TINYUI_FEATURE_LIQUID_GLASS_TEXT
2
+
3
+ #import <React/RCTViewComponentView.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @interface TinyuiLiquidGlassTextView : RCTViewComponentView
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END
11
+
12
+ #endif