react-native-tinyui 0.4.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 +32 -31
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextProvider.swift +64 -79
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.h +46 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.mm +159 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.mm +6 -27
- package/ios/LiquidGlassText/TinyuiTextOutlineShape.swift +5 -2
- 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 +2 -12
- package/lib/module/LiquidGlassText/index.js +60 -35
- package/lib/module/LiquidGlassText/index.js.map +1 -1
- package/lib/module/Menu/MenuNativeComponent.ts +6 -2
- package/lib/module/Menu/options.js +2 -2
- package/lib/module/Menu/options.js.map +1 -1
- package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts +2 -8
- package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/LiquidGlassText/index.d.ts +1 -1
- package/lib/typescript/src/LiquidGlassText/index.d.ts.map +1 -1
- package/lib/typescript/src/LiquidGlassText/types.d.ts +13 -22
- package/lib/typescript/src/LiquidGlassText/types.d.ts.map +1 -1
- package/lib/typescript/src/Menu/MenuNativeComponent.d.ts +2 -2
- package/lib/typescript/src/Menu/MenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/Menu/options.d.ts +41 -1
- package/lib/typescript/src/Menu/options.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/LiquidGlassText/LiquidGlassTextNativeComponent.ts +2 -12
- package/src/LiquidGlassText/index.tsx +73 -39
- package/src/LiquidGlassText/types.ts +24 -50
- package/src/Menu/MenuNativeComponent.ts +6 -2
- package/src/Menu/options.ts +6 -2
- package/src/index.tsx +2 -4
package/README.md
CHANGED
|
@@ -167,8 +167,8 @@ while the popover is visible.
|
|
|
167
167
|
## LiquidGlassText
|
|
168
168
|
|
|
169
169
|
`LiquidGlassText` renders native glass inside the glyph outlines, based on
|
|
170
|
-
[GlassText](https://github.com/ailtonvivaz/GlassText). It
|
|
171
|
-
|
|
170
|
+
[GlassText](https://github.com/ailtonvivaz/GlassText). It exposes the effect and
|
|
171
|
+
typography through React props.
|
|
172
172
|
Background image loading is not part of this component.
|
|
173
173
|
|
|
174
174
|
Building this component requires **Xcode 26+**. Glass is rendered on **iOS 26+**;
|
|
@@ -180,48 +180,49 @@ import { LiquidGlassText } from 'react-native-tinyui';
|
|
|
180
180
|
|
|
181
181
|
<LiquidGlassText
|
|
182
182
|
text="Liquid Glass"
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
effect="regular"
|
|
184
|
+
tint="#00BBDD"
|
|
185
|
+
interactive
|
|
186
|
+
fontDesign="rounded"
|
|
187
|
+
textStyle={{ fontSize: 48, fontWeight: '700' }}
|
|
185
188
|
/>;
|
|
186
189
|
|
|
187
190
|
<LiquidGlassText
|
|
188
191
|
text={'Multi-line\nGlass Text\nEffect'}
|
|
189
|
-
glass="clear"
|
|
190
|
-
font="largeTitle"
|
|
191
|
-
fontWeight="heavy"
|
|
192
192
|
fontDesign="serif"
|
|
193
|
+
textStyle={{ fontSize: 34, fontWeight: '800' }}
|
|
193
194
|
multilineTextAlignment="center"
|
|
194
195
|
/>;
|
|
195
196
|
```
|
|
196
197
|
|
|
197
|
-
| Prop | Values
|
|
198
|
-
| ------------------------ |
|
|
199
|
-
| `text` | String (including text translated in JavaScript)
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `fontDesign` | `default`, `serif`, `monospaced`, `rounded`
|
|
204
|
-
| `
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
`
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
`
|
|
215
|
-
effect, with no tint or interactivity. `identity` applies no glass effect.
|
|
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
216
|
The component also accepts standard `ViewProps`, including `style`, `testID`,
|
|
217
217
|
accessibility props and a native view ref. The supplied text is used for
|
|
218
218
|
the default accessibility label; pass `accessibilityLabel` to override it.
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
produce a
|
|
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.
|
|
225
226
|
|
|
226
227
|
`text` accepts a string. Applications using JavaScript localization can pass
|
|
227
228
|
their translated string directly.
|
|
@@ -2,60 +2,64 @@ import CoreText
|
|
|
2
2
|
import SwiftUI
|
|
3
3
|
import UIKit
|
|
4
4
|
|
|
5
|
-
private struct LiquidGlassTextConfiguration
|
|
6
|
-
struct
|
|
7
|
-
let
|
|
8
|
-
let
|
|
9
|
-
let
|
|
10
|
-
let
|
|
11
|
-
let
|
|
5
|
+
private struct LiquidGlassTextConfiguration {
|
|
6
|
+
struct TextStyleConfiguration {
|
|
7
|
+
let fontSize: CGFloat?
|
|
8
|
+
let fontFamily: String?
|
|
9
|
+
let fontWeight: String?
|
|
10
|
+
let fontStyle: String?
|
|
11
|
+
let letterSpacing: CGFloat?
|
|
12
|
+
|
|
13
|
+
init(_ dictionary: [String: Any]) {
|
|
14
|
+
fontSize = (dictionary["fontSize"] as? NSNumber).map { CGFloat($0.doubleValue) }
|
|
15
|
+
fontFamily = dictionary["fontFamily"] as? String
|
|
16
|
+
fontWeight = dictionary["fontWeight"] as? String
|
|
17
|
+
fontStyle = dictionary["fontStyle"] as? String
|
|
18
|
+
letterSpacing = (dictionary["letterSpacing"] as? NSNumber).map { CGFloat($0.doubleValue) }
|
|
19
|
+
}
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
var text: String?
|
|
15
|
-
var
|
|
23
|
+
var effect: String?
|
|
16
24
|
var interactive: Bool?
|
|
17
|
-
var font: FontConfiguration?
|
|
18
|
-
var fontWeight: String?
|
|
19
25
|
var fontDesign: String?
|
|
26
|
+
var textStyle: TextStyleConfiguration?
|
|
20
27
|
var multilineTextAlignment: String?
|
|
21
28
|
|
|
29
|
+
init(_ dictionary: [String: Any] = [:]) {
|
|
30
|
+
text = dictionary["text"] as? String
|
|
31
|
+
effect = dictionary["effect"] as? String
|
|
32
|
+
interactive = dictionary["interactive"] as? Bool
|
|
33
|
+
fontDesign = dictionary["fontDesign"] as? String
|
|
34
|
+
textStyle = (dictionary["textStyle"] as? [String: Any]).map(TextStyleConfiguration.init)
|
|
35
|
+
multilineTextAlignment = dictionary["multilineTextAlignment"] as? String
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
var resolvedFont: Font {
|
|
23
39
|
let design: Font.Design
|
|
24
|
-
switch fontDesign
|
|
40
|
+
switch fontDesign {
|
|
25
41
|
case "serif": design = .serif
|
|
26
42
|
case "monospaced": design = .monospaced
|
|
27
43
|
case "rounded": design = .rounded
|
|
28
44
|
default: design = .default
|
|
29
45
|
}
|
|
30
46
|
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
case "largeTitle": style = .largeTitle
|
|
34
|
-
case "title": style = .title
|
|
35
|
-
case "title2": style = .title2
|
|
36
|
-
case "title3": style = .title3
|
|
37
|
-
case "headline": style = .headline
|
|
38
|
-
case "subheadline": style = .subheadline
|
|
39
|
-
case "callout": style = .callout
|
|
40
|
-
case "footnote": style = .footnote
|
|
41
|
-
case "caption": style = .caption
|
|
42
|
-
case "caption2": style = .caption2
|
|
43
|
-
default: style = .body
|
|
44
|
-
}
|
|
45
|
-
|
|
47
|
+
let size = textStyle?.fontSize
|
|
48
|
+
let family = textStyle?.fontFamily
|
|
46
49
|
var result: Font
|
|
47
|
-
if let
|
|
48
|
-
if let
|
|
50
|
+
if let family, !family.isEmpty {
|
|
51
|
+
if let size, size.isFinite, size > 0 {
|
|
49
52
|
result = .custom(family, fixedSize: size)
|
|
50
53
|
} else {
|
|
51
|
-
result = .
|
|
54
|
+
result = .custom(family, size: 17, relativeTo: .body)
|
|
52
55
|
}
|
|
56
|
+
} else if let size, size.isFinite, size > 0 {
|
|
57
|
+
result = .system(size: size, design: design)
|
|
53
58
|
} else {
|
|
54
|
-
result = .system(
|
|
59
|
+
result = .system(.body, design: design)
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
if let weight = fontWeight ?? font?.weight {
|
|
62
|
+
if let weight = textStyle?.fontWeight {
|
|
59
63
|
switch weight {
|
|
60
64
|
case "ultraLight": result = result.weight(.ultraLight)
|
|
61
65
|
case "thin": result = result.weight(.thin)
|
|
@@ -68,6 +72,9 @@ private struct LiquidGlassTextConfiguration: Decodable {
|
|
|
68
72
|
default: result = result.weight(.regular)
|
|
69
73
|
}
|
|
70
74
|
}
|
|
75
|
+
if textStyle?.fontStyle == "italic" {
|
|
76
|
+
result = result.italic()
|
|
77
|
+
}
|
|
71
78
|
return result
|
|
72
79
|
}
|
|
73
80
|
|
|
@@ -78,6 +85,11 @@ private struct LiquidGlassTextConfiguration: Decodable {
|
|
|
78
85
|
default: return .leading
|
|
79
86
|
}
|
|
80
87
|
}
|
|
88
|
+
|
|
89
|
+
var resolvedLetterSpacing: CGFloat {
|
|
90
|
+
guard let letterSpacing = textStyle?.letterSpacing, letterSpacing.isFinite else { return 0 }
|
|
91
|
+
return letterSpacing
|
|
92
|
+
}
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
private struct LiquidGlassTextContent: View {
|
|
@@ -90,12 +102,14 @@ private struct LiquidGlassTextContent: View {
|
|
|
90
102
|
if #available(iOS 26.0, *) {
|
|
91
103
|
LiquidGlassTextEffect(
|
|
92
104
|
text: text,
|
|
93
|
-
effect: configuration.
|
|
105
|
+
effect: configuration.effect ?? "clear",
|
|
94
106
|
tintColor: tintColor,
|
|
95
|
-
interactive: configuration.interactive ?? false
|
|
107
|
+
interactive: configuration.interactive ?? false,
|
|
108
|
+
letterSpacing: configuration.resolvedLetterSpacing
|
|
96
109
|
)
|
|
97
110
|
} else {
|
|
98
111
|
Text(verbatim: text)
|
|
112
|
+
.tracking(configuration.resolvedLetterSpacing)
|
|
99
113
|
.foregroundStyle(tintColor.map { Color(uiColor: $0) } ?? .primary)
|
|
100
114
|
}
|
|
101
115
|
}
|
|
@@ -114,6 +128,7 @@ private struct LiquidGlassTextEffect: View {
|
|
|
114
128
|
let effect: String
|
|
115
129
|
let tintColor: UIColor?
|
|
116
130
|
let interactive: Bool
|
|
131
|
+
let letterSpacing: CGFloat
|
|
117
132
|
|
|
118
133
|
@Environment(\.font) private var font
|
|
119
134
|
@Environment(\.fontResolutionContext) private var fontResolutionContext
|
|
@@ -141,14 +156,21 @@ private struct LiquidGlassTextEffect: View {
|
|
|
141
156
|
let width = lines.reduce(CGFloat.zero) { width, line in
|
|
142
157
|
let attributed = NSAttributedString(
|
|
143
158
|
string: line,
|
|
144
|
-
attributes: [
|
|
159
|
+
attributes: [
|
|
160
|
+
kCTFontAttributeName as NSAttributedString.Key: ctFont,
|
|
161
|
+
kCTKernAttributeName as NSAttributedString.Key: letterSpacing,
|
|
162
|
+
]
|
|
145
163
|
)
|
|
146
164
|
return max(width, CGFloat(CTLineGetTypographicBounds(
|
|
147
165
|
CTLineCreateWithAttributedString(attributed), nil, nil, nil
|
|
148
166
|
)))
|
|
149
167
|
}
|
|
150
168
|
let shape = TinyuiTextOutlineShape(
|
|
151
|
-
text: text,
|
|
169
|
+
text: text,
|
|
170
|
+
ctFont: ctFont,
|
|
171
|
+
fontSize: resolved.pointSize,
|
|
172
|
+
alignment: alignment,
|
|
173
|
+
letterSpacing: letterSpacing
|
|
152
174
|
)
|
|
153
175
|
Rectangle()
|
|
154
176
|
.fill(.clear)
|
|
@@ -157,44 +179,22 @@ private struct LiquidGlassTextEffect: View {
|
|
|
157
179
|
}
|
|
158
180
|
}
|
|
159
181
|
|
|
160
|
-
@objc public protocol TinyuiLiquidGlassTextViewDelegate: AnyObject {
|
|
161
|
-
func onContentSizeChange(width: Double, height: Double, configuration: String)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
182
|
@objc public class TinyuiLiquidGlassTextProvider: UIView {
|
|
165
|
-
private weak var delegate: TinyuiLiquidGlassTextViewDelegate?
|
|
166
183
|
private var hostingController: UIHostingController<LiquidGlassTextContent>?
|
|
167
184
|
private var configuration = LiquidGlassTextConfiguration()
|
|
168
|
-
private var configurationJSON = "{}"
|
|
169
185
|
private var tintColorValue: UIColor?
|
|
170
|
-
private var lastSize: CGSize?
|
|
171
186
|
|
|
172
187
|
@objc public weak var parentViewController: UIViewController? {
|
|
173
188
|
didSet { setNeedsLayout() }
|
|
174
189
|
}
|
|
175
190
|
|
|
176
|
-
@objc public
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
view.invalidateMeasurement()
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
@objc public func configure(_ json: String, tintColor: UIColor?) {
|
|
186
|
-
guard json != configurationJSON || tintColor != tintColorValue else { return }
|
|
187
|
-
configurationJSON = json
|
|
188
|
-
configuration = (try? JSONDecoder().decode(
|
|
189
|
-
LiquidGlassTextConfiguration.self, from: Data(json.utf8)
|
|
190
|
-
)) ?? LiquidGlassTextConfiguration()
|
|
191
|
+
@objc public func configure(
|
|
192
|
+
_ dictionary: [String: Any],
|
|
193
|
+
tintColor: UIColor?
|
|
194
|
+
) {
|
|
195
|
+
configuration = LiquidGlassTextConfiguration(dictionary)
|
|
191
196
|
tintColorValue = tintColor
|
|
192
197
|
hostingController?.rootView = content
|
|
193
|
-
invalidateMeasurement()
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
@objc public func invalidateMeasurement() {
|
|
197
|
-
lastSize = nil
|
|
198
198
|
setNeedsLayout()
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -207,7 +207,7 @@ private struct LiquidGlassTextEffect: View {
|
|
|
207
207
|
if window == nil {
|
|
208
208
|
detachController()
|
|
209
209
|
} else {
|
|
210
|
-
|
|
210
|
+
setNeedsLayout()
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -221,10 +221,8 @@ private struct LiquidGlassTextEffect: View {
|
|
|
221
221
|
@objc public func reset() {
|
|
222
222
|
detachController()
|
|
223
223
|
parentViewController = nil
|
|
224
|
-
configurationJSON = "{}"
|
|
225
224
|
configuration = LiquidGlassTextConfiguration()
|
|
226
225
|
tintColorValue = nil
|
|
227
|
-
lastSize = nil
|
|
228
226
|
}
|
|
229
227
|
|
|
230
228
|
override public func layoutSubviews() {
|
|
@@ -248,18 +246,5 @@ private struct LiquidGlassTextEffect: View {
|
|
|
248
246
|
controller.didMove(toParent: parentViewController)
|
|
249
247
|
}
|
|
250
248
|
controller.view.frame = bounds
|
|
251
|
-
|
|
252
|
-
// GlassText uses explicit newlines and intrinsic sizing, never automatic wrapping.
|
|
253
|
-
let measured = controller.sizeThatFits(in: CGSize(width: 100_000, height: 100_000))
|
|
254
|
-
let scale = traitCollection.displayScale > 0 ? traitCollection.displayScale : 1
|
|
255
|
-
let size = CGSize(
|
|
256
|
-
width: ceil(measured.width * scale) / scale,
|
|
257
|
-
height: ceil(measured.height * scale) / scale
|
|
258
|
-
)
|
|
259
|
-
guard size.width.isFinite, size.height.isFinite, size != lastSize else { return }
|
|
260
|
-
lastSize = size
|
|
261
|
-
delegate?.onContentSizeChange(
|
|
262
|
-
width: size.width, height: size.height, configuration: configurationJSON
|
|
263
|
-
)
|
|
264
249
|
}
|
|
265
250
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <react/renderer/components/TinyuiSpec/ShadowNodes.h>
|
|
4
|
+
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
|
|
5
|
+
#include <react/renderer/core/ConcreteComponentDescriptor.h>
|
|
6
|
+
|
|
7
|
+
namespace facebook::react {
|
|
8
|
+
|
|
9
|
+
class TinyuiLiquidGlassTextShadowNode final
|
|
10
|
+
: public ConcreteViewShadowNode<
|
|
11
|
+
TinyuiLiquidGlassTextViewComponentName,
|
|
12
|
+
TinyuiLiquidGlassTextViewProps,
|
|
13
|
+
TinyuiLiquidGlassTextViewEventEmitter,
|
|
14
|
+
TinyuiLiquidGlassTextViewState> {
|
|
15
|
+
public:
|
|
16
|
+
using BaseShadowNode = ConcreteViewShadowNode<
|
|
17
|
+
TinyuiLiquidGlassTextViewComponentName,
|
|
18
|
+
TinyuiLiquidGlassTextViewProps,
|
|
19
|
+
TinyuiLiquidGlassTextViewEventEmitter,
|
|
20
|
+
TinyuiLiquidGlassTextViewState>;
|
|
21
|
+
using BaseShadowNode::BaseShadowNode;
|
|
22
|
+
|
|
23
|
+
static ShadowNodeTraits BaseTraits()
|
|
24
|
+
{
|
|
25
|
+
auto traits = BaseShadowNode::BaseTraits();
|
|
26
|
+
traits.set(ShadowNodeTraits::Trait::LeafYogaNode);
|
|
27
|
+
traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode);
|
|
28
|
+
return traits;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Size measureContent(
|
|
32
|
+
const LayoutContext &layoutContext,
|
|
33
|
+
const LayoutConstraints &layoutConstraints) const override;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
class TinyuiLiquidGlassTextComponentDescriptor final
|
|
37
|
+
: public ConcreteComponentDescriptor<TinyuiLiquidGlassTextShadowNode> {
|
|
38
|
+
public:
|
|
39
|
+
TinyuiLiquidGlassTextComponentDescriptor(
|
|
40
|
+
const ComponentDescriptorParameters ¶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
|
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
#import <React/RCTConversions.h>
|
|
6
6
|
#import <React/UIView+React.h>
|
|
7
|
-
#import <react/
|
|
8
|
-
#import <react/renderer/components/TinyuiSpec/EventEmitters.h>
|
|
7
|
+
#import <react/utils/FollyConvert.h>
|
|
9
8
|
#import <react/renderer/components/TinyuiSpec/Props.h>
|
|
10
9
|
#import <react/renderer/components/TinyuiSpec/RCTComponentViewHelpers.h>
|
|
11
10
|
#import "RCTFabricComponentsPlugins.h"
|
|
12
11
|
#import "Tinyui-Swift.h"
|
|
12
|
+
#import "TinyuiLiquidGlassTextShadowNode.h"
|
|
13
13
|
|
|
14
14
|
using namespace facebook::react;
|
|
15
15
|
|
|
16
|
-
@interface TinyuiLiquidGlassTextView () <RCTTinyuiLiquidGlassTextViewViewProtocol
|
|
17
|
-
TinyuiLiquidGlassTextViewDelegate>
|
|
16
|
+
@interface TinyuiLiquidGlassTextView () <RCTTinyuiLiquidGlassTextViewViewProtocol>
|
|
18
17
|
@end
|
|
19
18
|
|
|
20
19
|
@implementation TinyuiLiquidGlassTextView {
|
|
@@ -23,7 +22,7 @@ using namespace facebook::react;
|
|
|
23
22
|
|
|
24
23
|
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
25
24
|
{
|
|
26
|
-
return concreteComponentDescriptorProvider<
|
|
25
|
+
return concreteComponentDescriptorProvider<TinyuiLiquidGlassTextComponentDescriptor>();
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
- (instancetype)initWithFrame:(CGRect)frame
|
|
@@ -31,7 +30,7 @@ using namespace facebook::react;
|
|
|
31
30
|
if (self = [super initWithFrame:frame]) {
|
|
32
31
|
static const auto defaultProps = std::make_shared<const TinyuiLiquidGlassTextViewProps>();
|
|
33
32
|
_props = defaultProps;
|
|
34
|
-
_provider = [[TinyuiLiquidGlassTextProvider alloc]
|
|
33
|
+
_provider = [[TinyuiLiquidGlassTextProvider alloc] initWithFrame:CGRectZero];
|
|
35
34
|
self.contentView = _provider;
|
|
36
35
|
}
|
|
37
36
|
return self;
|
|
@@ -48,31 +47,11 @@ using namespace facebook::react;
|
|
|
48
47
|
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
49
48
|
{
|
|
50
49
|
const auto &newProps = *std::static_pointer_cast<TinyuiLiquidGlassTextViewProps const>(props);
|
|
51
|
-
[_provider configure:
|
|
50
|
+
[_provider configure:convertFollyDynamicToId(newProps.configuration)
|
|
52
51
|
tintColor:RCTUIColorFromSharedColor(newProps.tintColor)];
|
|
53
52
|
[super updateProps:props oldProps:oldProps];
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
- (void)updateEventEmitter:(EventEmitter::Shared const &)eventEmitter
|
|
57
|
-
{
|
|
58
|
-
[super updateEventEmitter:eventEmitter];
|
|
59
|
-
[_provider invalidateMeasurement];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
- (void)onContentSizeChangeWithWidth:(double)width
|
|
63
|
-
height:(double)height
|
|
64
|
-
configuration:(NSString *)configuration
|
|
65
|
-
{
|
|
66
|
-
if (_eventEmitter == nullptr) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
std::static_pointer_cast<const TinyuiLiquidGlassTextViewEventEmitter>(_eventEmitter)
|
|
70
|
-
->onContentSizeChange(TinyuiLiquidGlassTextViewEventEmitter::OnContentSizeChange{
|
|
71
|
-
.width = width,
|
|
72
|
-
.height = height,
|
|
73
|
-
.configuration = RCTStringFromNSString(configuration)});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
55
|
- (void)prepareForRecycle
|
|
77
56
|
{
|
|
78
57
|
[_provider reset];
|
|
@@ -16,6 +16,7 @@ struct TinyuiTextOutlineShape: Shape {
|
|
|
16
16
|
var ctFont: CTFont
|
|
17
17
|
var fontSize: CGFloat
|
|
18
18
|
var alignment: TextAlignment = .center
|
|
19
|
+
var letterSpacing: CGFloat = 0
|
|
19
20
|
|
|
20
21
|
func path(in rect: CGRect) -> Path {
|
|
21
22
|
// Split into explicit lines (no wrapping here)
|
|
@@ -25,9 +26,11 @@ struct TinyuiTextOutlineShape: Shape {
|
|
|
25
26
|
var ctLines: [CTLine] = []
|
|
26
27
|
ctLines.reserveCapacity(rawLines.count)
|
|
27
28
|
|
|
28
|
-
let attrKey = kCTFontAttributeName as NSAttributedString.Key
|
|
29
29
|
for l in rawLines {
|
|
30
|
-
let attr
|
|
30
|
+
let attr: [NSAttributedString.Key: Any] = [
|
|
31
|
+
kCTFontAttributeName as NSAttributedString.Key: ctFont,
|
|
32
|
+
kCTKernAttributeName as NSAttributedString.Key: letterSpacing,
|
|
33
|
+
]
|
|
31
34
|
let astr = NSAttributedString(string: l, attributes: attr)
|
|
32
35
|
let line = CTLineCreateWithAttributedString(astr)
|
|
33
36
|
ctLines.append(line)
|
|
@@ -31,8 +31,8 @@ import UIKit
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
///
|
|
35
|
-
@objc public var menuConfig: String =
|
|
34
|
+
/// Menu configuration passed to the SwiftUI view.
|
|
35
|
+
@objc public var menuConfig: [String: Any] = [:] {
|
|
36
36
|
didSet {
|
|
37
37
|
props.menuConfig = menuConfig
|
|
38
38
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
#import <React/RCTConversions.h>
|
|
6
6
|
#import <React/UIView+React.h>
|
|
7
|
+
#import <react/utils/FollyConvert.h>
|
|
7
8
|
|
|
8
9
|
#import <react/renderer/components/TinyuiSpec/ComponentDescriptors.h>
|
|
9
10
|
#import <react/renderer/components/TinyuiSpec/EventEmitters.h>
|
|
@@ -82,7 +83,7 @@ using namespace facebook::react;
|
|
|
82
83
|
const auto &newMenuProps = *std::static_pointer_cast<TinyuiMenuViewProps const>(props);
|
|
83
84
|
|
|
84
85
|
if (oldMenuProps.menuConfig != newMenuProps.menuConfig) {
|
|
85
|
-
_menuProvider.menuConfig =
|
|
86
|
+
_menuProvider.menuConfig = convertFollyDynamicToId(newMenuProps.menuConfig);
|
|
86
87
|
}
|
|
87
88
|
if (oldMenuProps.title != newMenuProps.title) {
|
|
88
89
|
_menuProvider.title = RCTNSStringFromString(newMenuProps.title);
|