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.
- package/README.md +70 -2
- package/Tinyui.podspec +7 -1
- package/ios/Core/Tinyui.mm +4 -0
- package/ios/Core/TinyuiComponentClasses.mm +35 -9
- package/ios/LiquidGlassText/GlassText-LICENSE +21 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextProvider.swift +250 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.h +46 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.mm +159 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.h +12 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.mm +63 -0
- package/ios/LiquidGlassText/TinyuiTextOutlineShape.swift +116 -0
- package/ios/Menu/TinyuiMenuProvider.swift +2 -2
- package/ios/Menu/TinyuiMenuView.mm +2 -1
- package/ios/Menu/TinyuiMenuViewSwift.swift +42 -124
- package/lib/module/LiquidGlassText/LiquidGlassTextNativeComponent.ts +16 -0
- package/lib/module/LiquidGlassText/index.js +87 -0
- package/lib/module/LiquidGlassText/index.js.map +1 -0
- package/lib/module/LiquidGlassText/types.js +4 -0
- package/lib/module/LiquidGlassText/types.js.map +1 -0
- package/lib/module/{MenuNativeComponent.ts → Menu/MenuNativeComponent.ts} +6 -2
- package/lib/module/{Menu.js → Menu/index.js} +3 -3
- package/lib/module/Menu/index.js.map +1 -0
- package/lib/module/{MenuOptions.js → Menu/options.js} +3 -3
- package/lib/module/Menu/options.js.map +1 -0
- package/lib/module/Menu/types.js +4 -0
- package/lib/module/Menu/types.js.map +1 -0
- package/lib/module/{PopoverPrimitives.js → Popover/PopoverClose.js} +1 -1
- package/lib/module/Popover/PopoverClose.js.map +1 -0
- package/lib/module/{Popover.js → Popover/index.js} +3 -3
- package/lib/module/Popover/index.js.map +1 -0
- package/lib/module/Popover/types.js +4 -0
- package/lib/module/Popover/types.js.map +1 -0
- package/lib/module/index.js +4 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/{getNativeTinyui.js → utils/getNativeTinyui.js} +1 -1
- package/lib/module/utils/getNativeTinyui.js.map +1 -0
- package/lib/module/utils/index.js +4 -0
- package/lib/module/utils/index.js.map +1 -0
- package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts +8 -0
- package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/LiquidGlassText/index.d.ts +4 -0
- package/lib/typescript/src/LiquidGlassText/index.d.ts.map +1 -0
- package/lib/typescript/src/LiquidGlassText/types.d.ts +22 -0
- package/lib/typescript/src/LiquidGlassText/types.d.ts.map +1 -0
- package/lib/typescript/src/{MenuNativeComponent.d.ts → Menu/MenuNativeComponent.d.ts} +2 -2
- package/lib/typescript/src/Menu/MenuNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/{Menu.d.ts → Menu/index.d.ts} +3 -3
- package/lib/typescript/src/Menu/index.d.ts.map +1 -0
- package/lib/typescript/src/Menu/options.d.ts +47 -0
- package/lib/typescript/src/Menu/options.d.ts.map +1 -0
- package/lib/typescript/src/{Menu.types.d.ts → Menu/types.d.ts} +1 -1
- package/lib/typescript/src/Menu/types.d.ts.map +1 -0
- package/lib/typescript/src/{PopoverPrimitives.d.ts → Popover/PopoverClose.d.ts} +2 -2
- package/lib/typescript/src/Popover/PopoverClose.d.ts.map +1 -0
- package/lib/typescript/src/Popover/PopoverNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/Popover/index.d.ts +8 -0
- package/lib/typescript/src/Popover/index.d.ts.map +1 -0
- package/lib/typescript/src/{Popover.types.d.ts → Popover/types.d.ts} +1 -1
- package/lib/typescript/src/Popover/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +8 -6
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/{getNativeTinyui.d.ts → utils/getNativeTinyui.d.ts} +2 -2
- package/lib/typescript/src/utils/getNativeTinyui.d.ts.map +1 -0
- package/lib/typescript/src/utils/index.d.ts +3 -0
- package/lib/typescript/src/utils/index.d.ts.map +1 -0
- package/package.json +19 -10
- package/src/LiquidGlassText/LiquidGlassTextNativeComponent.ts +16 -0
- package/src/LiquidGlassText/index.tsx +113 -0
- package/src/LiquidGlassText/types.ts +37 -0
- package/src/{MenuNativeComponent.ts → Menu/MenuNativeComponent.ts} +6 -2
- package/src/{Menu.tsx → Menu/index.tsx} +4 -4
- package/src/{MenuOptions.ts → Menu/options.ts} +7 -3
- package/src/{PopoverPrimitives.tsx → Popover/PopoverClose.tsx} +1 -1
- package/src/{Popover.tsx → Popover/index.tsx} +12 -3
- package/src/index.tsx +12 -4
- package/src/react-native.d.ts +1 -0
- package/src/{getNativeTinyui.ts → utils/getNativeTinyui.ts} +2 -2
- package/src/utils/index.ts +2 -0
- package/lib/module/Menu.js.map +0 -1
- package/lib/module/Menu.types.js +0 -4
- package/lib/module/Menu.types.js.map +0 -1
- package/lib/module/MenuOptions.js.map +0 -1
- package/lib/module/Popover.js.map +0 -1
- package/lib/module/Popover.types.js +0 -4
- package/lib/module/Popover.types.js.map +0 -1
- package/lib/module/PopoverPrimitives.js.map +0 -1
- package/lib/module/getNativeTinyui.js.map +0 -1
- package/lib/typescript/src/Menu.d.ts.map +0 -1
- package/lib/typescript/src/Menu.types.d.ts.map +0 -1
- package/lib/typescript/src/MenuNativeComponent.d.ts.map +0 -1
- package/lib/typescript/src/MenuOptions.d.ts +0 -7
- package/lib/typescript/src/MenuOptions.d.ts.map +0 -1
- package/lib/typescript/src/Popover.d.ts +0 -7
- package/lib/typescript/src/Popover.d.ts.map +0 -1
- package/lib/typescript/src/Popover.types.d.ts.map +0 -1
- package/lib/typescript/src/PopoverNativeComponent.d.ts.map +0 -1
- package/lib/typescript/src/PopoverPrimitives.d.ts.map +0 -1
- package/lib/typescript/src/getNativeTinyui.d.ts.map +0 -1
- /package/lib/module/{PopoverNativeComponent.ts → Popover/PopoverNativeComponent.ts} +0 -0
- /package/lib/typescript/src/{PopoverNativeComponent.d.ts → Popover/PopoverNativeComponent.d.ts} +0 -0
- /package/src/{Menu.types.ts → Menu/types.ts} +0 -0
- /package/src/{PopoverNativeComponent.ts → Popover/PopoverNativeComponent.ts} +0 -0
- /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 `
|
|
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
|
-
|
|
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 = {
|
package/ios/Core/Tinyui.mm
CHANGED
|
@@ -1,27 +1,53 @@
|
|
|
1
|
-
#import <React/
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 ¶meters)
|
|
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
|