react-native-tinyui 0.2.0 → 0.4.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 (98) hide show
  1. package/README.md +69 -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 +265 -0
  7. package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.h +12 -0
  8. package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.mm +84 -0
  9. package/ios/LiquidGlassText/TinyuiTextOutlineShape.swift +113 -0
  10. package/ios/Menu/TinyuiMenuViewSwift.swift +10 -19
  11. package/lib/module/LiquidGlassText/LiquidGlassTextNativeComponent.ts +26 -0
  12. package/lib/module/LiquidGlassText/index.js +62 -0
  13. package/lib/module/LiquidGlassText/index.js.map +1 -0
  14. package/lib/module/LiquidGlassText/types.js +4 -0
  15. package/lib/module/LiquidGlassText/types.js.map +1 -0
  16. package/lib/module/{Menu.js → Menu/index.js} +3 -3
  17. package/lib/module/Menu/index.js.map +1 -0
  18. package/lib/module/{MenuOptions.js → Menu/options.js} +1 -1
  19. package/lib/module/Menu/options.js.map +1 -0
  20. package/lib/module/Menu/types.js +4 -0
  21. package/lib/module/Menu/types.js.map +1 -0
  22. package/lib/module/{PopoverPrimitives.js → Popover/PopoverClose.js} +1 -1
  23. package/lib/module/Popover/PopoverClose.js.map +1 -0
  24. package/lib/module/{Popover.js → Popover/index.js} +3 -3
  25. package/lib/module/Popover/index.js.map +1 -0
  26. package/lib/module/Popover/types.js +4 -0
  27. package/lib/module/Popover/types.js.map +1 -0
  28. package/lib/module/index.js +4 -3
  29. package/lib/module/index.js.map +1 -1
  30. package/lib/module/{getNativeTinyui.js → utils/getNativeTinyui.js} +1 -1
  31. package/lib/module/utils/getNativeTinyui.js.map +1 -0
  32. package/lib/module/utils/index.js +4 -0
  33. package/lib/module/utils/index.js.map +1 -0
  34. package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts +14 -0
  35. package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts.map +1 -0
  36. package/lib/typescript/src/LiquidGlassText/index.d.ts +4 -0
  37. package/lib/typescript/src/LiquidGlassText/index.d.ts.map +1 -0
  38. package/lib/typescript/src/LiquidGlassText/types.d.ts +31 -0
  39. package/lib/typescript/src/LiquidGlassText/types.d.ts.map +1 -0
  40. package/lib/typescript/src/Menu/MenuNativeComponent.d.ts.map +1 -0
  41. package/lib/typescript/src/{Menu.d.ts → Menu/index.d.ts} +3 -3
  42. package/lib/typescript/src/Menu/index.d.ts.map +1 -0
  43. package/lib/typescript/src/{MenuOptions.d.ts → Menu/options.d.ts} +2 -2
  44. package/lib/typescript/src/Menu/options.d.ts.map +1 -0
  45. package/lib/typescript/src/{Menu.types.d.ts → Menu/types.d.ts} +1 -1
  46. package/lib/typescript/src/Menu/types.d.ts.map +1 -0
  47. package/lib/typescript/src/{PopoverPrimitives.d.ts → Popover/PopoverClose.d.ts} +2 -2
  48. package/lib/typescript/src/Popover/PopoverClose.d.ts.map +1 -0
  49. package/lib/typescript/src/Popover/PopoverNativeComponent.d.ts.map +1 -0
  50. package/lib/typescript/src/Popover/index.d.ts +8 -0
  51. package/lib/typescript/src/Popover/index.d.ts.map +1 -0
  52. package/lib/typescript/src/{Popover.types.d.ts → Popover/types.d.ts} +1 -1
  53. package/lib/typescript/src/Popover/types.d.ts.map +1 -0
  54. package/lib/typescript/src/index.d.ts +8 -6
  55. package/lib/typescript/src/index.d.ts.map +1 -1
  56. package/lib/typescript/src/{getNativeTinyui.d.ts → utils/getNativeTinyui.d.ts} +2 -2
  57. package/lib/typescript/src/utils/getNativeTinyui.d.ts.map +1 -0
  58. package/lib/typescript/src/utils/index.d.ts +3 -0
  59. package/lib/typescript/src/utils/index.d.ts.map +1 -0
  60. package/package.json +19 -10
  61. package/src/LiquidGlassText/LiquidGlassTextNativeComponent.ts +26 -0
  62. package/src/LiquidGlassText/index.tsx +79 -0
  63. package/src/LiquidGlassText/types.ts +63 -0
  64. package/src/{Menu.tsx → Menu/index.tsx} +4 -4
  65. package/src/{MenuOptions.ts → Menu/options.ts} +1 -1
  66. package/src/{PopoverPrimitives.tsx → Popover/PopoverClose.tsx} +1 -1
  67. package/src/{Popover.tsx → Popover/index.tsx} +12 -3
  68. package/src/index.tsx +14 -4
  69. package/src/react-native.d.ts +1 -0
  70. package/src/{getNativeTinyui.ts → utils/getNativeTinyui.ts} +2 -2
  71. package/src/utils/index.ts +2 -0
  72. package/lib/module/Menu.js.map +0 -1
  73. package/lib/module/Menu.types.js +0 -4
  74. package/lib/module/Menu.types.js.map +0 -1
  75. package/lib/module/MenuOptions.js.map +0 -1
  76. package/lib/module/Popover.js.map +0 -1
  77. package/lib/module/Popover.types.js +0 -4
  78. package/lib/module/Popover.types.js.map +0 -1
  79. package/lib/module/PopoverPrimitives.js.map +0 -1
  80. package/lib/module/getNativeTinyui.js.map +0 -1
  81. package/lib/typescript/src/Menu.d.ts.map +0 -1
  82. package/lib/typescript/src/Menu.types.d.ts.map +0 -1
  83. package/lib/typescript/src/MenuNativeComponent.d.ts.map +0 -1
  84. package/lib/typescript/src/MenuOptions.d.ts.map +0 -1
  85. package/lib/typescript/src/Popover.d.ts +0 -7
  86. package/lib/typescript/src/Popover.d.ts.map +0 -1
  87. package/lib/typescript/src/Popover.types.d.ts.map +0 -1
  88. package/lib/typescript/src/PopoverNativeComponent.d.ts.map +0 -1
  89. package/lib/typescript/src/PopoverPrimitives.d.ts.map +0 -1
  90. package/lib/typescript/src/getNativeTinyui.d.ts.map +0 -1
  91. /package/lib/module/{MenuNativeComponent.ts → Menu/MenuNativeComponent.ts} +0 -0
  92. /package/lib/module/{PopoverNativeComponent.ts → Popover/PopoverNativeComponent.ts} +0 -0
  93. /package/lib/typescript/src/{MenuNativeComponent.d.ts → Menu/MenuNativeComponent.d.ts} +0 -0
  94. /package/lib/typescript/src/{PopoverNativeComponent.d.ts → Popover/PopoverNativeComponent.d.ts} +0 -0
  95. /package/src/{MenuNativeComponent.ts → Menu/MenuNativeComponent.ts} +0 -0
  96. /package/src/{Menu.types.ts → Menu/types.ts} +0 -0
  97. /package/src/{PopoverNativeComponent.ts → Popover/PopoverNativeComponent.ts} +0 -0
  98. /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,71 @@ 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 supports the reference
171
+ library's text, glass, font and alignment APIs 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
+ glass={{ effect: 'regular', tint: '#00BBDD' }}
184
+ font={{ size: 48, weight: 'bold', design: 'rounded' }}
185
+ />;
186
+
187
+ <LiquidGlassText
188
+ text={'Multi-line\nGlass Text\nEffect'}
189
+ glass="clear"
190
+ font="largeTitle"
191
+ fontWeight="heavy"
192
+ fontDesign="serif"
193
+ multilineTextAlignment="center"
194
+ />;
195
+ ```
196
+
197
+ | Prop | Values | Default |
198
+ | ------------------------ | ---------------------------------------------------------------------------------------- | ------------- |
199
+ | `text` | String (including text translated in JavaScript) | Required |
200
+ | `glass` | `'clear'`, `'regular'`, `'identity'`, or `{ effect?, tint?, interactive? }` | `'clear'` |
201
+ | `font` | A semantic text style, or `{ size, weight?, design?, family? }` | `'body'` |
202
+ | `fontWeight` | `ultraLight`, `thin`, `light`, `regular`, `medium`, `semibold`, `bold`, `heavy`, `black` | Font's weight |
203
+ | `fontDesign` | `default`, `serif`, `monospaced`, `rounded` | Font's design |
204
+ | `multilineTextAlignment` | `leading`, `center`, `trailing` | `leading` |
205
+
206
+ Semantic font styles are `largeTitle`, `title`, `title2`, `title3`, `headline`,
207
+ `subheadline`, `body`, `callout`, `footnote`, `caption`, and `caption2`. They
208
+ follow SwiftUI Dynamic Type. Explicit `size` values use fixed point sizes;
209
+ `family` accepts the PostScript name of a font installed in the app.
210
+ `fontWeight` and `fontDesign` override the corresponding system font options.
211
+ A custom font family determines its own design.
212
+
213
+ Glass options accept `tint` as a React Native `ColorValue`, including
214
+ `PlatformColor` and `DynamicColorIOS`. The object form defaults to the clear
215
+ effect, with no tint or interactivity. `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
+ Native content measurements supply the default width and height. Layout styles
221
+ can override these dimensions, but do not resize the font or wrap text. As in
222
+ GlassText, use explicit `\n` characters for multiple lines. Keep padding on a
223
+ surrounding `View`. Color emoji and other glyphs without vector outlines cannot
224
+ produce a glass shape.
225
+
226
+ `text` accepts a string. Applications using JavaScript localization can pass
227
+ their translated string directly.
228
+
229
+ The adapted Core Text outline implementation retains the upstream MIT notice in
230
+ [`ios/LiquidGlassText/GlassText-LICENSE`](ios/LiquidGlassText/GlassText-LICENSE).
231
+
165
232
  ## Contributing
166
233
 
167
234
  - [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,265 @@
1
+ import CoreText
2
+ import SwiftUI
3
+ import UIKit
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?
12
+ }
13
+
14
+ var text: String?
15
+ var glass: String?
16
+ var interactive: Bool?
17
+ var font: FontConfiguration?
18
+ var fontWeight: String?
19
+ var fontDesign: String?
20
+ var multilineTextAlignment: String?
21
+
22
+ var resolvedFont: Font {
23
+ let design: Font.Design
24
+ switch fontDesign ?? font?.design {
25
+ case "serif": design = .serif
26
+ case "monospaced": design = .monospaced
27
+ case "rounded": design = .rounded
28
+ default: design = .default
29
+ }
30
+
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
+
46
+ var result: Font
47
+ if let size = font?.size, size.isFinite, size > 0 {
48
+ if let family = font?.family, !family.isEmpty {
49
+ result = .custom(family, fixedSize: size)
50
+ } else {
51
+ result = .system(size: size, design: design)
52
+ }
53
+ } else {
54
+ result = .system(style, design: design)
55
+ }
56
+
57
+ // Preserve a semantic style's default weight (e.g. headline) unless overridden.
58
+ if let weight = fontWeight ?? font?.weight {
59
+ switch weight {
60
+ case "ultraLight": result = result.weight(.ultraLight)
61
+ case "thin": result = result.weight(.thin)
62
+ case "light": result = result.weight(.light)
63
+ case "medium": result = result.weight(.medium)
64
+ case "semibold": result = result.weight(.semibold)
65
+ case "bold": result = result.weight(.bold)
66
+ case "heavy": result = result.weight(.heavy)
67
+ case "black": result = result.weight(.black)
68
+ default: result = result.weight(.regular)
69
+ }
70
+ }
71
+ return result
72
+ }
73
+
74
+ var alignment: TextAlignment {
75
+ switch multilineTextAlignment {
76
+ case "center": return .center
77
+ case "trailing": return .trailing
78
+ default: return .leading
79
+ }
80
+ }
81
+ }
82
+
83
+ private struct LiquidGlassTextContent: View {
84
+ let configuration: LiquidGlassTextConfiguration
85
+ let text: String
86
+ let tintColor: UIColor?
87
+
88
+ var body: some View {
89
+ Group {
90
+ if #available(iOS 26.0, *) {
91
+ LiquidGlassTextEffect(
92
+ text: text,
93
+ effect: configuration.glass ?? "clear",
94
+ tintColor: tintColor,
95
+ interactive: configuration.interactive ?? false
96
+ )
97
+ } else {
98
+ Text(verbatim: text)
99
+ .foregroundStyle(tintColor.map { Color(uiColor: $0) } ?? .primary)
100
+ }
101
+ }
102
+ .font(configuration.resolvedFont)
103
+ .multilineTextAlignment(configuration.alignment)
104
+ .fixedSize()
105
+ // React Native exposes one accessible text element with the supplied label.
106
+ .accessibilityHidden(true)
107
+ }
108
+ }
109
+
110
+ // Adapted from ailtonvivaz/GlassText (MIT). See GlassText-LICENSE.
111
+ @available(iOS 26.0, *)
112
+ private struct LiquidGlassTextEffect: View {
113
+ let text: String
114
+ let effect: String
115
+ let tintColor: UIColor?
116
+ let interactive: Bool
117
+
118
+ @Environment(\.font) private var font
119
+ @Environment(\.fontResolutionContext) private var fontResolutionContext
120
+ @Environment(\.multilineTextAlignment) private var alignment
121
+
122
+ private var glass: Glass {
123
+ var result: Glass
124
+ switch effect {
125
+ case "regular": result = .regular
126
+ case "identity": result = .identity
127
+ default: result = .clear
128
+ }
129
+ if let tintColor {
130
+ result = result.tint(Color(uiColor: tintColor))
131
+ }
132
+ return result.interactive(interactive)
133
+ }
134
+
135
+ var body: some View {
136
+ let resolved = (font ?? .body).resolve(in: fontResolutionContext)
137
+ let ctFont = resolved.ctFont
138
+ let lines = text.components(separatedBy: CharacterSet.newlines)
139
+ let lineHeight = CTFontGetAscent(ctFont) + CTFontGetDescent(ctFont)
140
+ + max(CTFontGetLeading(ctFont), resolved.pointSize * 0.2)
141
+ let width = lines.reduce(CGFloat.zero) { width, line in
142
+ let attributed = NSAttributedString(
143
+ string: line,
144
+ attributes: [kCTFontAttributeName as NSAttributedString.Key: ctFont]
145
+ )
146
+ return max(width, CGFloat(CTLineGetTypographicBounds(
147
+ CTLineCreateWithAttributedString(attributed), nil, nil, nil
148
+ )))
149
+ }
150
+ let shape = TinyuiTextOutlineShape(
151
+ text: text, ctFont: ctFont, fontSize: resolved.pointSize, alignment: alignment
152
+ )
153
+ Rectangle()
154
+ .fill(.clear)
155
+ .glassEffect(glass, in: shape)
156
+ .frame(width: width, height: text.isEmpty ? 0 : lineHeight * CGFloat(lines.count))
157
+ }
158
+ }
159
+
160
+ @objc public protocol TinyuiLiquidGlassTextViewDelegate: AnyObject {
161
+ func onContentSizeChange(width: Double, height: Double, configuration: String)
162
+ }
163
+
164
+ @objc public class TinyuiLiquidGlassTextProvider: UIView {
165
+ private weak var delegate: TinyuiLiquidGlassTextViewDelegate?
166
+ private var hostingController: UIHostingController<LiquidGlassTextContent>?
167
+ private var configuration = LiquidGlassTextConfiguration()
168
+ private var configurationJSON = "{}"
169
+ private var tintColorValue: UIColor?
170
+ private var lastSize: CGSize?
171
+
172
+ @objc public weak var parentViewController: UIViewController? {
173
+ didSet { setNeedsLayout() }
174
+ }
175
+
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
+ tintColorValue = tintColor
192
+ hostingController?.rootView = content
193
+ invalidateMeasurement()
194
+ }
195
+
196
+ @objc public func invalidateMeasurement() {
197
+ lastSize = nil
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
+ invalidateMeasurement()
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
+ configurationJSON = "{}"
225
+ configuration = LiquidGlassTextConfiguration()
226
+ tintColorValue = nil
227
+ lastSize = nil
228
+ }
229
+
230
+ override public func layoutSubviews() {
231
+ super.layoutSubviews()
232
+ guard window != nil, let parentViewController else { return }
233
+
234
+ if hostingController == nil {
235
+ let controller = UIHostingController(rootView: content)
236
+ controller.safeAreaRegions = []
237
+ controller.view.backgroundColor = .clear
238
+ parentViewController.addChild(controller)
239
+ addSubview(controller.view)
240
+ controller.didMove(toParent: parentViewController)
241
+ hostingController = controller
242
+ }
243
+ guard let controller = hostingController else { return }
244
+ if controller.parent !== parentViewController {
245
+ controller.willMove(toParent: nil)
246
+ controller.removeFromParent()
247
+ parentViewController.addChild(controller)
248
+ controller.didMove(toParent: parentViewController)
249
+ }
250
+ 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
+ }
265
+ }
@@ -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
@@ -0,0 +1,84 @@
1
+ #if TINYUI_FEATURE_LIQUID_GLASS_TEXT
2
+
3
+ #import "TinyuiLiquidGlassTextView.h"
4
+
5
+ #import <React/RCTConversions.h>
6
+ #import <React/UIView+React.h>
7
+ #import <react/renderer/components/TinyuiSpec/ComponentDescriptors.h>
8
+ #import <react/renderer/components/TinyuiSpec/EventEmitters.h>
9
+ #import <react/renderer/components/TinyuiSpec/Props.h>
10
+ #import <react/renderer/components/TinyuiSpec/RCTComponentViewHelpers.h>
11
+ #import "RCTFabricComponentsPlugins.h"
12
+ #import "Tinyui-Swift.h"
13
+
14
+ using namespace facebook::react;
15
+
16
+ @interface TinyuiLiquidGlassTextView () <RCTTinyuiLiquidGlassTextViewViewProtocol,
17
+ TinyuiLiquidGlassTextViewDelegate>
18
+ @end
19
+
20
+ @implementation TinyuiLiquidGlassTextView {
21
+ TinyuiLiquidGlassTextProvider *_provider;
22
+ }
23
+
24
+ + (ComponentDescriptorProvider)componentDescriptorProvider
25
+ {
26
+ return concreteComponentDescriptorProvider<TinyuiLiquidGlassTextViewComponentDescriptor>();
27
+ }
28
+
29
+ - (instancetype)initWithFrame:(CGRect)frame
30
+ {
31
+ if (self = [super initWithFrame:frame]) {
32
+ static const auto defaultProps = std::make_shared<const TinyuiLiquidGlassTextViewProps>();
33
+ _props = defaultProps;
34
+ _provider = [[TinyuiLiquidGlassTextProvider alloc] initWithDelegate:self];
35
+ self.contentView = _provider;
36
+ }
37
+ return self;
38
+ }
39
+
40
+ - (void)didMoveToWindow
41
+ {
42
+ [super didMoveToWindow];
43
+ if (self.window != nil) {
44
+ _provider.parentViewController = [self reactViewController];
45
+ }
46
+ }
47
+
48
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
49
+ {
50
+ const auto &newProps = *std::static_pointer_cast<TinyuiLiquidGlassTextViewProps const>(props);
51
+ [_provider configure:RCTNSStringFromString(newProps.configuration)
52
+ tintColor:RCTUIColorFromSharedColor(newProps.tintColor)];
53
+ [super updateProps:props oldProps:oldProps];
54
+ }
55
+
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
+ - (void)prepareForRecycle
77
+ {
78
+ [_provider reset];
79
+ [super prepareForRecycle];
80
+ }
81
+
82
+ @end
83
+
84
+ #endif
@@ -0,0 +1,113 @@
1
+ // Adapted from ailtonvivaz/GlassText (MIT). See GlassText-LICENSE.
2
+ //
3
+ // TextOutlineShape.swift
4
+ // GlassText
5
+ //
6
+ // Created by Ailton Vieira on 17/08/25.
7
+ //
8
+
9
+ import CoreGraphics
10
+ @preconcurrency import CoreText
11
+ import SwiftUI
12
+
13
+ /// A SwiftUI Shape that creates text outline paths for glass morphism effects
14
+ struct TinyuiTextOutlineShape: Shape {
15
+ var text: String
16
+ var ctFont: CTFont
17
+ var fontSize: CGFloat
18
+ var alignment: TextAlignment = .center
19
+
20
+ func path(in rect: CGRect) -> Path {
21
+ // Split into explicit lines (no wrapping here)
22
+ let rawLines = text.components(separatedBy: CharacterSet.newlines)
23
+
24
+ // Pre-create CTLine objects to leverage CoreText shaping for complex scripts
25
+ var ctLines: [CTLine] = []
26
+ ctLines.reserveCapacity(rawLines.count)
27
+
28
+ let attrKey = kCTFontAttributeName as NSAttributedString.Key
29
+ for l in rawLines {
30
+ let attr = [attrKey: ctFont]
31
+ let astr = NSAttributedString(string: l, attributes: attr)
32
+ let line = CTLineCreateWithAttributedString(astr)
33
+ ctLines.append(line)
34
+ }
35
+
36
+ // Metrics (we still stack lines with a uniform height derived from base font)
37
+ let ascent = CTFontGetAscent(ctFont)
38
+ let descent = CTFontGetDescent(ctFont)
39
+ let leading = CTFontGetLeading(ctFont)
40
+ let lineHeight = ascent + descent + max(leading, CTFontGetSize(ctFont) * 0.2)
41
+
42
+ // Determine widths using typographic bounds for each CTLine (better for scripts & ligatures)
43
+ var lineWidths: [CGFloat] = []
44
+ var maxWidth: CGFloat = 0
45
+ for line in ctLines {
46
+ let w = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil))
47
+ lineWidths.append(w)
48
+ maxWidth = max(maxWidth, w)
49
+ }
50
+
51
+ // Build combined path aggregating each run's glyphs & positions
52
+ let combined = CGMutablePath()
53
+ for (idx, line) in ctLines.enumerated() {
54
+ // Position baseline for this line (stack downward later flipped)
55
+ let penY = -CGFloat(idx) * lineHeight
56
+ let lineWidth = lineWidths[idx]
57
+
58
+ // Alignment offset
59
+ let startX: CGFloat
60
+ switch alignment {
61
+ case .leading: startX = 0
62
+ case .center: startX = (maxWidth - lineWidth) / 2
63
+ case .trailing: startX = maxWidth - lineWidth
64
+ }
65
+
66
+ // Iterate glyph runs (handles complex scripts, clusters & ligatures)
67
+ let runs = CTLineGetGlyphRuns(line) as NSArray
68
+ for runObj in runs {
69
+ let run = runObj as! CTRun // Safe cast (CoreFoundation bridging)
70
+ let glyphCount = CTRunGetGlyphCount(run)
71
+ if glyphCount == 0 { continue }
72
+
73
+ // Extract glyphs & positions
74
+ var glyphs = Array(repeating: CGGlyph(), count: glyphCount)
75
+ var positions = Array(repeating: CGPoint.zero, count: glyphCount)
76
+ CTRunGetGlyphs(run, CFRange(location: 0, length: 0), &glyphs)
77
+ CTRunGetPositions(run, CFRange(location: 0, length: 0), &positions)
78
+
79
+ // Run font (may differ due to fallback)
80
+ let attrs = CTRunGetAttributes(run) as NSDictionary
81
+ let runFont = (attrs[kCTFontAttributeName] as! CTFont)
82
+
83
+ for i in 0 ..< glyphCount {
84
+ let g = glyphs[i]
85
+ guard let gPath = CTFontCreatePathForGlyph(runFont, g, nil) else { continue }
86
+ var t = CGAffineTransform(
87
+ translationX: startX + positions[i].x,
88
+ y: penY + positions[i].y
89
+ )
90
+ if let shifted = gPath.copy(using: &t) {
91
+ combined.addPath(shifted)
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ if combined.isEmpty { return Path() }
98
+
99
+ // Normalize & center similar to previous approach
100
+ let bbox = combined.boundingBoxOfPath
101
+ var toOrigin = CGAffineTransform(translationX: -bbox.minX, y: -bbox.minY)
102
+ let norm = combined.copy(using: &toOrigin) ?? combined
103
+ var flip = CGAffineTransform(scaleX: 1, y: -1)
104
+ let flipped = norm.copy(using: &flip) ?? norm
105
+ let finalBBox = flipped.boundingBoxOfPath
106
+ var centerT = CGAffineTransform(
107
+ translationX: rect.midX - finalBBox.midX,
108
+ y: rect.midY - finalBBox.midY
109
+ )
110
+ let finalCG = flipped.copy(using: &centerT) ?? flipped
111
+ return Path(finalCG)
112
+ }
113
+ }