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.
Files changed (34) hide show
  1. package/README.md +32 -31
  2. package/ios/LiquidGlassText/TinyuiLiquidGlassTextProvider.swift +64 -79
  3. package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.h +46 -0
  4. package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.mm +159 -0
  5. package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.mm +6 -27
  6. package/ios/LiquidGlassText/TinyuiTextOutlineShape.swift +5 -2
  7. package/ios/Menu/TinyuiMenuProvider.swift +2 -2
  8. package/ios/Menu/TinyuiMenuView.mm +2 -1
  9. package/ios/Menu/TinyuiMenuViewSwift.swift +42 -124
  10. package/lib/module/LiquidGlassText/LiquidGlassTextNativeComponent.ts +2 -12
  11. package/lib/module/LiquidGlassText/index.js +60 -35
  12. package/lib/module/LiquidGlassText/index.js.map +1 -1
  13. package/lib/module/Menu/MenuNativeComponent.ts +6 -2
  14. package/lib/module/Menu/options.js +2 -2
  15. package/lib/module/Menu/options.js.map +1 -1
  16. package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts +2 -8
  17. package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts.map +1 -1
  18. package/lib/typescript/src/LiquidGlassText/index.d.ts +1 -1
  19. package/lib/typescript/src/LiquidGlassText/index.d.ts.map +1 -1
  20. package/lib/typescript/src/LiquidGlassText/types.d.ts +13 -22
  21. package/lib/typescript/src/LiquidGlassText/types.d.ts.map +1 -1
  22. package/lib/typescript/src/Menu/MenuNativeComponent.d.ts +2 -2
  23. package/lib/typescript/src/Menu/MenuNativeComponent.d.ts.map +1 -1
  24. package/lib/typescript/src/Menu/options.d.ts +41 -1
  25. package/lib/typescript/src/Menu/options.d.ts.map +1 -1
  26. package/lib/typescript/src/index.d.ts +1 -1
  27. package/lib/typescript/src/index.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/LiquidGlassText/LiquidGlassTextNativeComponent.ts +2 -12
  30. package/src/LiquidGlassText/index.tsx +73 -39
  31. package/src/LiquidGlassText/types.ts +24 -50
  32. package/src/Menu/MenuNativeComponent.ts +6 -2
  33. package/src/Menu/options.ts +6 -2
  34. 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 supports the reference
171
- library's text, glass, font and alignment APIs through React props.
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
- glass={{ effect: 'regular', tint: '#00BBDD' }}
184
- font={{ size: 48, weight: 'bold', design: 'rounded' }}
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 | 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.
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
- 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.
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: 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?
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 glass: String?
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 ?? font?.design {
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 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
-
47
+ let size = textStyle?.fontSize
48
+ let family = textStyle?.fontFamily
46
49
  var result: Font
47
- if let size = font?.size, size.isFinite, size > 0 {
48
- if let family = font?.family, !family.isEmpty {
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 = .system(size: size, design: design)
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(style, design: design)
59
+ result = .system(.body, design: design)
55
60
  }
56
61
 
57
- // Preserve a semantic style's default weight (e.g. headline) unless overridden.
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.glass ?? "clear",
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: [kCTFontAttributeName as NSAttributedString.Key: ctFont]
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, ctFont: ctFont, fontSize: resolved.pointSize, alignment: alignment
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 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
+ @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
- invalidateMeasurement()
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 &parameters)
41
+ : ConcreteComponentDescriptor(parameters)
42
+ {
43
+ }
44
+ };
45
+
46
+ } // namespace facebook::react
@@ -0,0 +1,159 @@
1
+ #import <CoreText/CoreText.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ #include <algorithm>
5
+ #include <cmath>
6
+ #include <string>
7
+
8
+ #include <react/renderer/core/LayoutConstraints.h>
9
+ #include <react/renderer/core/LayoutContext.h>
10
+
11
+ #include "TinyuiLiquidGlassTextShadowNode.h"
12
+
13
+ namespace facebook::react {
14
+
15
+ static std::string stringValue(
16
+ const folly::dynamic &object,
17
+ folly::StringPiece key,
18
+ const std::string &fallback = "")
19
+ {
20
+ if (!object.isObject()) {
21
+ return fallback;
22
+ }
23
+ auto value = object.getDefault(key, fallback);
24
+ return value.isString() ? value.getString() : fallback;
25
+ }
26
+
27
+ static CGFloat numberValue(
28
+ const folly::dynamic &object,
29
+ folly::StringPiece key,
30
+ CGFloat fallback)
31
+ {
32
+ if (!object.isObject()) {
33
+ return fallback;
34
+ }
35
+ auto value = object.getDefault(key, fallback);
36
+ return value.isNumber() ? static_cast<CGFloat>(value.asDouble()) : fallback;
37
+ }
38
+
39
+ static UIFontWeight fontWeight(const std::string &value)
40
+ {
41
+ if (value == "ultraLight") return UIFontWeightUltraLight;
42
+ if (value == "thin") return UIFontWeightThin;
43
+ if (value == "light") return UIFontWeightLight;
44
+ if (value == "medium") return UIFontWeightMedium;
45
+ if (value == "semibold") return UIFontWeightSemibold;
46
+ if (value == "bold") return UIFontWeightBold;
47
+ if (value == "heavy") return UIFontWeightHeavy;
48
+ if (value == "black") return UIFontWeightBlack;
49
+ return UIFontWeightRegular;
50
+ }
51
+
52
+ static UIFontDescriptorSystemDesign fontDesign(const std::string &value)
53
+ {
54
+ if (value == "serif") return UIFontDescriptorSystemDesignSerif;
55
+ if (value == "monospaced") return UIFontDescriptorSystemDesignMonospaced;
56
+ if (value == "rounded") return UIFontDescriptorSystemDesignRounded;
57
+ return nil;
58
+ }
59
+
60
+ static UIFont *resolvedFont(
61
+ const folly::dynamic &configuration,
62
+ const folly::dynamic &textStyle,
63
+ Float fontSizeMultiplier)
64
+ {
65
+ auto configuredSize = numberValue(textStyle, "fontSize", 0);
66
+ auto multiplier = std::isfinite(fontSizeMultiplier) && fontSizeMultiplier > 0
67
+ ? fontSizeMultiplier
68
+ : 1;
69
+ auto size = std::isfinite(configuredSize) && configuredSize > 0
70
+ ? configuredSize
71
+ : 17 * multiplier;
72
+ auto family = stringValue(textStyle, "fontFamily");
73
+ auto weight = stringValue(textStyle, "fontWeight");
74
+
75
+ UIFont *font = nil;
76
+ if (!family.empty()) {
77
+ NSString *familyName = [NSString stringWithUTF8String:family.c_str()];
78
+ font = familyName == nil ? nil : [UIFont fontWithName:familyName size:size];
79
+ }
80
+ if (font == nil) {
81
+ font = [UIFont systemFontOfSize:size weight:fontWeight(weight)];
82
+ }
83
+
84
+ UIFontDescriptor *descriptor = font.fontDescriptor;
85
+ if (family.empty()) {
86
+ auto design = fontDesign(stringValue(configuration, "fontDesign"));
87
+ if (design != nil) {
88
+ descriptor = [descriptor fontDescriptorWithDesign:design] ?: descriptor;
89
+ }
90
+ }
91
+
92
+ if (!weight.empty() && !family.empty()) {
93
+ descriptor = [descriptor fontDescriptorByAddingAttributes:@{
94
+ UIFontDescriptorTraitsAttribute: @{UIFontWeightTrait: @(fontWeight(weight))}
95
+ }];
96
+ }
97
+ if (stringValue(textStyle, "fontStyle") == "italic") {
98
+ descriptor = [descriptor fontDescriptorWithSymbolicTraits:
99
+ descriptor.symbolicTraits | UIFontDescriptorTraitItalic] ?: descriptor;
100
+ }
101
+ return [UIFont fontWithDescriptor:descriptor size:size];
102
+ }
103
+
104
+ Size TinyuiLiquidGlassTextShadowNode::measureContent(
105
+ const LayoutContext &layoutContext,
106
+ const LayoutConstraints &layoutConstraints) const
107
+ {
108
+ @autoreleasepool {
109
+ const auto &configuration = getConcreteProps().configuration;
110
+ if (!configuration.isObject()) {
111
+ return layoutConstraints.clamp({});
112
+ }
113
+
114
+ auto textValue = stringValue(configuration, "text");
115
+ if (textValue.empty()) {
116
+ return layoutConstraints.clamp({});
117
+ }
118
+ NSString *text = [NSString stringWithUTF8String:textValue.c_str()] ?: @"";
119
+ auto textStyle = configuration.getDefault("textStyle", folly::dynamic::object);
120
+ UIFont *font = resolvedFont(configuration, textStyle, layoutContext.fontSizeMultiplier);
121
+ auto letterSpacing = numberValue(textStyle, "letterSpacing", 0);
122
+ if (!std::isfinite(letterSpacing)) {
123
+ letterSpacing = 0;
124
+ }
125
+
126
+ CTFontRef ctFont = CTFontCreateWithName(
127
+ (__bridge CFStringRef)font.fontName,
128
+ font.pointSize,
129
+ nullptr);
130
+ auto lineHeight = CTFontGetAscent(ctFont) + CTFontGetDescent(ctFont) +
131
+ std::max(CTFontGetLeading(ctFont), CTFontGetSize(ctFont) * 0.2);
132
+ CGFloat width = 0;
133
+ auto lines = [text componentsSeparatedByCharactersInSet:NSCharacterSet.newlineCharacterSet];
134
+ NSDictionary<NSAttributedStringKey, id> *attributes = @{
135
+ (__bridge NSAttributedStringKey)kCTFontAttributeName: (__bridge id)ctFont,
136
+ (__bridge NSAttributedStringKey)kCTKernAttributeName: @(letterSpacing),
137
+ };
138
+ for (NSString *line in lines) {
139
+ auto attributed = [[NSAttributedString alloc] initWithString:line attributes:attributes];
140
+ CTLineRef ctLine = CTLineCreateWithAttributedString(
141
+ (__bridge CFAttributedStringRef)attributed);
142
+ width = std::max(
143
+ width,
144
+ static_cast<CGFloat>(CTLineGetTypographicBounds(ctLine, nullptr, nullptr, nullptr)));
145
+ CFRelease(ctLine);
146
+ }
147
+ CFRelease(ctFont);
148
+
149
+ auto height = lineHeight * lines.count;
150
+ auto scale = layoutContext.pointScaleFactor > 0 ? layoutContext.pointScaleFactor : 1;
151
+ Size measured{
152
+ .width = static_cast<Float>(std::ceil(width * scale) / scale),
153
+ .height = static_cast<Float>(std::ceil(height * scale) / scale),
154
+ };
155
+ return layoutConstraints.clamp(measured);
156
+ }
157
+ }
158
+
159
+ } // namespace facebook::react
@@ -4,17 +4,16 @@
4
4
 
5
5
  #import <React/RCTConversions.h>
6
6
  #import <React/UIView+React.h>
7
- #import <react/renderer/components/TinyuiSpec/ComponentDescriptors.h>
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<TinyuiLiquidGlassTextViewComponentDescriptor>();
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] initWithDelegate:self];
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:RCTNSStringFromString(newProps.configuration)
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 = [attrKey: ctFont]
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
- /// JSON menu configuration passed to the SwiftUI view.
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 = RCTNSStringFromString(newMenuProps.menuConfig);
86
+ _menuProvider.menuConfig = convertFollyDynamicToId(newMenuProps.menuConfig);
86
87
  }
87
88
  if (oldMenuProps.title != newMenuProps.title) {
88
89
  _menuProvider.title = RCTNSStringFromString(newMenuProps.title);