react-native-tinyui 0.3.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.
- package/README.md +69 -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 +265 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.h +12 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.mm +84 -0
- package/ios/LiquidGlassText/TinyuiTextOutlineShape.swift +113 -0
- package/lib/module/LiquidGlassText/LiquidGlassTextNativeComponent.ts +26 -0
- package/lib/module/LiquidGlassText/index.js +62 -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/{Menu.js → Menu/index.js} +3 -3
- package/lib/module/Menu/index.js.map +1 -0
- package/lib/module/{MenuOptions.js → Menu/options.js} +1 -1
- 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 +14 -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 +31 -0
- package/lib/typescript/src/LiquidGlassText/types.d.ts.map +1 -0
- 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/{MenuOptions.d.ts → Menu/options.d.ts} +2 -2
- 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 +26 -0
- package/src/LiquidGlassText/index.tsx +79 -0
- package/src/LiquidGlassText/types.ts +63 -0
- package/src/{Menu.tsx → Menu/index.tsx} +4 -4
- package/src/{MenuOptions.ts → Menu/options.ts} +1 -1
- package/src/{PopoverPrimitives.tsx → Popover/PopoverClose.tsx} +1 -1
- package/src/{Popover.tsx → Popover/index.tsx} +12 -3
- package/src/index.tsx +14 -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.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/{MenuNativeComponent.ts → Menu/MenuNativeComponent.ts} +0 -0
- /package/lib/module/{PopoverNativeComponent.ts → Popover/PopoverNativeComponent.ts} +0 -0
- /package/lib/typescript/src/{MenuNativeComponent.d.ts → Menu/MenuNativeComponent.d.ts} +0 -0
- /package/lib/typescript/src/{PopoverNativeComponent.d.ts → Popover/PopoverNativeComponent.d.ts} +0 -0
- /package/src/{MenuNativeComponent.ts → Menu/MenuNativeComponent.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,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 = {
|
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,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,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: ¢erT) ?? flipped
|
|
111
|
+
return Path(finalCG)
|
|
112
|
+
}
|
|
113
|
+
}
|