@onekeyfe/react-native-native-list 3.0.157-alpha.249 → 3.0.157-alpha.250
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 +21 -5
- package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +711 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActionRenderer.kt +169 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +243 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +20 -6
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +487 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +820 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListImageSlot.kt +147 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +453 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +997 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +234 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +232 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMetricCardRenderer.kt +551 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +24 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +475 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRailRenderer.kt +143 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +42 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +415 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +163 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowHost.kt +70 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +748 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSystemRenderer.kt +464 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +25 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +174 -104
- package/android/src/main/java/com/onekey/nativelist/NativeListWalletGroupRenderer.kt +407 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSelectorBackgroundColorTest.kt +34 -0
- package/docs/DESIGN.md +518 -9
- package/docs/MIGRATION_BASELINE.md +117 -0
- package/docs/ROW_TEMPLATES.md +359 -0
- package/docs/SPEC.md +358 -0
- package/docs/STYLE_SPEC.md +978 -0
- package/docs/images/list-capabilities.svg +39 -0
- package/docs/images/row-action.svg +17 -0
- package/docs/images/row-activity.svg +24 -0
- package/docs/images/row-data.svg +24 -0
- package/docs/images/row-identity.svg +25 -0
- package/docs/images/row-market.svg +23 -0
- package/docs/images/row-media-tile.svg +26 -0
- package/docs/images/row-message.svg +20 -0
- package/docs/images/row-metric-card.svg +33 -0
- package/docs/images/row-rail.svg +18 -0
- package/docs/images/row-section-header.svg +19 -0
- package/docs/images/row-style-regions.svg +30 -0
- package/docs/images/row-system.svg +30 -0
- package/docs/images/row-wallet-group.svg +26 -0
- package/ios/NativeListAccessoryStack.swift +680 -0
- package/ios/NativeListActionRenderer.swift +108 -0
- package/ios/NativeListActivityRenderer.swift +179 -0
- package/ios/NativeListDataRowRenderer.swift +444 -0
- package/ios/NativeListIdentityRenderer.swift +685 -0
- package/ios/NativeListImageSlot.swift +140 -0
- package/ios/NativeListLeadingVisual.swift +372 -0
- package/ios/NativeListMarketRenderer.swift +698 -0
- package/ios/NativeListMediaTileRenderer.swift +184 -0
- package/ios/NativeListMessageRenderer.swift +168 -0
- package/ios/NativeListMetricCardRenderer.swift +420 -0
- package/ios/NativeListModels.swift +32 -0
- package/ios/NativeListRailRenderer.swift +152 -0
- package/ios/NativeListRendererCell.swift +311 -0
- package/ios/NativeListRendererRegistry.swift +50 -0
- package/ios/NativeListResolvedText.swift +122 -0
- package/ios/NativeListRowHost.swift +63 -0
- package/ios/NativeListSectionHeaderRenderer.swift +708 -0
- package/ios/NativeListSystemRenderer.swift +568 -0
- package/ios/NativeListTextLabel.swift +42 -0
- package/ios/NativeListTextStyles.swift +236 -0
- package/ios/NativeListWalletGroupRenderer.swift +255 -0
- package/ios/RNCNativeListView.swift +111 -180
- package/lib/module/models.js +54 -0
- package/lib/module/validation.js +324 -26
- package/lib/module/web/NativeListWebEngine.js +148 -936
- package/lib/module/web/templates/ActionRowRenderer.js +97 -0
- package/lib/module/web/templates/ActivityRowRenderer.js +95 -0
- package/lib/module/web/templates/DataRowRenderer.js +145 -0
- package/lib/module/web/templates/IdentityRowRenderer.js +289 -0
- package/lib/module/web/templates/MarketRowRenderer.js +310 -0
- package/lib/module/web/templates/MediaTileRowRenderer.js +132 -0
- package/lib/module/web/templates/MessageRowRenderer.js +165 -0
- package/lib/module/web/templates/MetricCardRowRenderer.js +172 -0
- package/lib/module/web/templates/RailRowRenderer.js +98 -0
- package/lib/module/web/templates/RowContainerStyle.js +100 -0
- package/lib/module/web/templates/RowElements.js +93 -0
- package/lib/module/web/templates/RowRendererRegistry.js +70 -0
- package/lib/module/web/templates/RowText.js +121 -0
- package/lib/module/web/templates/RowVisual.js +43 -0
- package/lib/module/web/templates/SectionHeaderRowRenderer.js +146 -0
- package/lib/module/web/templates/SystemRowRenderer.js +161 -0
- package/lib/module/web/templates/WalletGroupRowRenderer.js +96 -0
- package/lib/typescript/src/models.d.ts +166 -13
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +18 -16
- package/lib/typescript/src/web/templates/ActionRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/ActivityRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/DataRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +16 -0
- package/lib/typescript/src/web/templates/MarketRowRenderer.d.ts +30 -0
- package/lib/typescript/src/web/templates/MediaTileRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +44 -0
- package/lib/typescript/src/web/templates/MetricCardRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/RailRowRenderer.d.ts +28 -0
- package/lib/typescript/src/web/templates/RowContainerStyle.d.ts +9 -0
- package/lib/typescript/src/web/templates/RowElements.d.ts +38 -0
- package/lib/typescript/src/web/templates/RowRendererRegistry.d.ts +83 -0
- package/lib/typescript/src/web/templates/RowText.d.ts +9 -0
- package/lib/typescript/src/web/templates/RowVisual.d.ts +20 -0
- package/lib/typescript/src/web/templates/SectionHeaderRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/SystemRowRenderer.d.ts +16 -0
- package/lib/typescript/src/web/templates/WalletGroupRowRenderer.d.ts +20 -0
- package/package.json +3 -3
- package/src/models.ts +281 -19
- package/src/validation.ts +501 -46
- package/src/web/NativeListWebEngine.ts +361 -1932
- package/src/web/templates/ActionRowRenderer.ts +123 -0
- package/src/web/templates/ActivityRowRenderer.ts +159 -0
- package/src/web/templates/DataRowRenderer.ts +212 -0
- package/src/web/templates/IdentityRowRenderer.ts +527 -0
- package/src/web/templates/MarketRowRenderer.ts +467 -0
- package/src/web/templates/MediaTileRowRenderer.ts +182 -0
- package/src/web/templates/MessageRowRenderer.ts +306 -0
- package/src/web/templates/MetricCardRowRenderer.ts +337 -0
- package/src/web/templates/RailRowRenderer.ts +136 -0
- package/src/web/templates/RowContainerStyle.ts +131 -0
- package/src/web/templates/RowElements.ts +160 -0
- package/src/web/templates/RowRendererRegistry.ts +111 -0
- package/src/web/templates/RowText.ts +181 -0
- package/src/web/templates/RowVisual.ts +115 -0
- package/src/web/templates/SectionHeaderRowRenderer.ts +243 -0
- package/src/web/templates/SystemRowRenderer.ts +282 -0
- package/src/web/templates/WalletGroupRowRenderer.ts +131 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +0 -4538
- package/ios/NativeListCell.swift +0 -4300
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
// OneKey patch: explicit summary actions use the source text's physical-pixel line box.
|
|
4
|
+
final class NativeListAccessoryButton: UIButton {
|
|
5
|
+
var pressedBackgroundColor: UIColor?
|
|
6
|
+
var rowTextOffsetY: CGFloat = 0
|
|
7
|
+
|
|
8
|
+
override var isHighlighted: Bool {
|
|
9
|
+
didSet {
|
|
10
|
+
guard let pressedBackgroundColor else { return }
|
|
11
|
+
backgroundColor = isHighlighted ? pressedBackgroundColor : .clear
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var selectorSummaryLineHeight: CGFloat? {
|
|
16
|
+
didSet {
|
|
17
|
+
invalidateIntrinsicContentSize()
|
|
18
|
+
setNeedsLayout()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var marketLineHeight: CGFloat? {
|
|
23
|
+
didSet {
|
|
24
|
+
invalidateIntrinsicContentSize()
|
|
25
|
+
setNeedsLayout()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private var sourcePixelScale: CGFloat {
|
|
30
|
+
max(1, window?.screen.scale ?? traitCollection.displayScale)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override var intrinsicContentSize: CGSize {
|
|
34
|
+
var size = super.intrinsicContentSize
|
|
35
|
+
guard selectorSummaryLineHeight != nil || marketLineHeight != nil,
|
|
36
|
+
let title = attributedTitle(for: .normal)
|
|
37
|
+
else { return size }
|
|
38
|
+
let width = title.boundingRect(
|
|
39
|
+
with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
|
|
40
|
+
options: [.usesLineFragmentOrigin, .usesFontLeading],
|
|
41
|
+
context: nil
|
|
42
|
+
).width
|
|
43
|
+
size.width = ceil(width * sourcePixelScale) / sourcePixelScale
|
|
44
|
+
return size
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override func layoutSubviews() {
|
|
48
|
+
super.layoutSubviews()
|
|
49
|
+
defer {
|
|
50
|
+
if let titleLabel {
|
|
51
|
+
var frame = titleLabel.frame
|
|
52
|
+
if contentVerticalAlignment == .top {
|
|
53
|
+
frame.origin.y = contentEdgeInsets.top
|
|
54
|
+
} else if contentVerticalAlignment == .bottom {
|
|
55
|
+
frame.origin.y = bounds.height - contentEdgeInsets.bottom - frame.height
|
|
56
|
+
}
|
|
57
|
+
frame.origin.y += rowTextOffsetY
|
|
58
|
+
titleLabel.frame = frame
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if let lineHeight = marketLineHeight, let titleLabel, let title = attributedTitle(for: .normal)
|
|
62
|
+
{
|
|
63
|
+
let measured = title.boundingRect(
|
|
64
|
+
with: CGSize(
|
|
65
|
+
width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
|
|
66
|
+
options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil
|
|
67
|
+
).width
|
|
68
|
+
let width = min(bounds.width, ceil(measured * sourcePixelScale) / sourcePixelScale)
|
|
69
|
+
let x =
|
|
70
|
+
contentHorizontalAlignment == .trailing
|
|
71
|
+
? bounds.width - width
|
|
72
|
+
: contentHorizontalAlignment == .leading ? 0 : (bounds.width - width) / 2
|
|
73
|
+
let textHeight =
|
|
74
|
+
titleLabel.numberOfLines > 1
|
|
75
|
+
? min(
|
|
76
|
+
CGFloat(titleLabel.numberOfLines) * lineHeight,
|
|
77
|
+
titleLabel.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
|
|
78
|
+
.height)
|
|
79
|
+
: lineHeight
|
|
80
|
+
titleLabel.frame = CGRect(
|
|
81
|
+
x: floor(x * sourcePixelScale) / sourcePixelScale,
|
|
82
|
+
y: (bounds.height - textHeight) / 2,
|
|
83
|
+
width: width, height: textHeight
|
|
84
|
+
)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
guard let lineHeight = selectorSummaryLineHeight, let titleLabel else { return }
|
|
88
|
+
// OneKey patch: position the final source line box after UIKit has measured the button.
|
|
89
|
+
let top = ceil((bounds.height - lineHeight) / 2 * sourcePixelScale) / sourcePixelScale
|
|
90
|
+
var frame = titleLabel.frame
|
|
91
|
+
frame.origin.y = top
|
|
92
|
+
titleLabel.frame = frame
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Bounded trailing controls shared by migrated templates. The owning host supplies
|
|
97
|
+
// action epochs; selection-only updates touch only the existing checkbox.
|
|
98
|
+
final class NativeListAccessoryStack: UIStackView {
|
|
99
|
+
private let accessoryButtons = (0..<2).map { _ in NativeListAccessoryButton(type: .system) }
|
|
100
|
+
private let checkboxButton = UIButton(type: .system)
|
|
101
|
+
private let spinner = UIActivityIndicatorView(style: .medium)
|
|
102
|
+
private var accessoryActions: [(String, NativeSelectionTarget?)] = []
|
|
103
|
+
private var checkboxAction: (String, NativeSelectionTarget?)?
|
|
104
|
+
private var boundCheckboxData: [String: Any]?
|
|
105
|
+
private var boundCheckboxTarget: NativeSelectionTarget?
|
|
106
|
+
private var semanticValueButtons: [UIButton] = []
|
|
107
|
+
private var accessorySizeConstraints: [NSLayoutConstraint] = []
|
|
108
|
+
private var currentItem: NativeListItem?
|
|
109
|
+
private var currentTheme: [String: Any]?
|
|
110
|
+
private var checkboxCheckedColor = UIColor.black
|
|
111
|
+
private var checkboxUncheckedColor = UIColor.white
|
|
112
|
+
private var checkboxIconColor = UIColor.white
|
|
113
|
+
private var checkboxBorderColor = UIColor.lightGray
|
|
114
|
+
var firstVisibleControl: UIView? { accessoryButtons.first { !$0.isHidden } }
|
|
115
|
+
var endInset: CGFloat?
|
|
116
|
+
var contentGap: CGFloat?
|
|
117
|
+
var onAction: ((String, UIView, Int?, NativeSelectionTarget?) -> Void)?
|
|
118
|
+
|
|
119
|
+
override init(frame: CGRect) {
|
|
120
|
+
super.init(frame: frame)
|
|
121
|
+
axis = .vertical
|
|
122
|
+
alignment = .trailing
|
|
123
|
+
setContentHuggingPriority(.required, for: .horizontal)
|
|
124
|
+
setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
125
|
+
for (index, button) in accessoryButtons.enumerated() {
|
|
126
|
+
button.tag = index
|
|
127
|
+
button.adjustsImageWhenDisabled = false
|
|
128
|
+
button.tintAdjustmentMode = .normal
|
|
129
|
+
button.addTarget(self, action: #selector(pressed(_:)), for: .touchUpInside)
|
|
130
|
+
addArrangedSubview(button)
|
|
131
|
+
}
|
|
132
|
+
checkboxButton.layer.borderWidth = 2
|
|
133
|
+
checkboxButton.layer.cornerRadius = 4
|
|
134
|
+
checkboxButton.imageView?.contentMode = .center
|
|
135
|
+
checkboxButton.addTarget(self, action: #selector(checkboxPressed), for: .touchUpInside)
|
|
136
|
+
NSLayoutConstraint.activate([
|
|
137
|
+
checkboxButton.widthAnchor.constraint(equalToConstant: 20),
|
|
138
|
+
checkboxButton.heightAnchor.constraint(equalToConstant: 20),
|
|
139
|
+
])
|
|
140
|
+
addArrangedSubview(checkboxButton)
|
|
141
|
+
addArrangedSubview(spinner)
|
|
142
|
+
}
|
|
143
|
+
required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
144
|
+
|
|
145
|
+
func bind(
|
|
146
|
+
_ item: NativeListItem, descriptors: [[String: Any]], theme: [String: Any]?,
|
|
147
|
+
style: [String: Any], defaultSpacing: CGFloat = 2,
|
|
148
|
+
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
149
|
+
) {
|
|
150
|
+
reset()
|
|
151
|
+
currentItem = item
|
|
152
|
+
currentTheme = theme
|
|
153
|
+
spacing = CGFloat(style.double("trailingGap", default: Double(defaultSpacing)))
|
|
154
|
+
checkboxCheckedColor = nativeListColor(theme, "primaryText", "#202020")
|
|
155
|
+
checkboxUncheckedColor = nativeListColor(theme, "inverseText", "#FCFCFC")
|
|
156
|
+
checkboxIconColor = checkboxUncheckedColor
|
|
157
|
+
checkboxBorderColor = UIColor(nativeListHex: "#00000031", fallback: .lightGray)
|
|
158
|
+
if item.data.string("presentation") == "networkSelector" {
|
|
159
|
+
checkboxCheckedColor = nativeListColor(theme, "checkboxBackground", "#202020")
|
|
160
|
+
checkboxBorderColor = nativeListColor(theme, "checkboxBorder", "#00000031")
|
|
161
|
+
checkboxIconColor = nativeListColor(theme, "checkboxIcon", "#FFFFFF")
|
|
162
|
+
checkboxUncheckedColor = checkboxIconColor
|
|
163
|
+
}
|
|
164
|
+
accessoryButtons.forEach {
|
|
165
|
+
$0.setTitleColor(nativeListColor(theme, "primaryText", "#202020"), for: .normal)
|
|
166
|
+
}
|
|
167
|
+
bindAccessories(item, descriptors, theme, checkboxState)
|
|
168
|
+
for (index, key) in ["value", "valueSecondary"].enumerated() {
|
|
169
|
+
if let value = style.dictionary(key), semanticValueButtons.indices.contains(index) {
|
|
170
|
+
applyStyledButton(semanticValueButtons[index], value)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
isHidden = arrangedSubviews.allSatisfy { $0.isHidden }
|
|
174
|
+
}
|
|
175
|
+
func updateSelection(
|
|
176
|
+
_ item: NativeListItem,
|
|
177
|
+
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
178
|
+
) {
|
|
179
|
+
currentItem = item
|
|
180
|
+
if boundCheckboxData != nil,
|
|
181
|
+
let data = item.data.dictionaries("trailing").last(where: { $0.string("kind") == "checkbox" })
|
|
182
|
+
?? item.data.dictionary("checkbox")
|
|
183
|
+
{
|
|
184
|
+
bindCheckbox(item, data, checkboxState)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
func anchorInset(for view: UIView) -> CGFloat {
|
|
188
|
+
currentItem?.data.string("presentation") == "accountSelector"
|
|
189
|
+
&& accessoryButtons.contains { $0 === view } && view.bounds.width == 38 ? 7 : 0
|
|
190
|
+
}
|
|
191
|
+
func reset() {
|
|
192
|
+
NSLayoutConstraint.deactivate(accessorySizeConstraints)
|
|
193
|
+
accessorySizeConstraints.removeAll()
|
|
194
|
+
accessoryActions.removeAll()
|
|
195
|
+
semanticValueButtons.removeAll()
|
|
196
|
+
checkboxAction = nil
|
|
197
|
+
boundCheckboxData = nil
|
|
198
|
+
boundCheckboxTarget = nil
|
|
199
|
+
endInset = nil
|
|
200
|
+
contentGap = nil
|
|
201
|
+
currentItem = nil
|
|
202
|
+
for (index, button) in accessoryButtons.enumerated() {
|
|
203
|
+
button.setAttributedTitle(nil, for: .normal)
|
|
204
|
+
button.setTitle(nil, for: .normal)
|
|
205
|
+
button.setImage(nil, for: .normal)
|
|
206
|
+
button.setImage(nil, for: .disabled)
|
|
207
|
+
button.titleLabel?.font = nativeListFont(
|
|
208
|
+
ofSize: index == 0 ? 16 : 14, weight: index == 0 ? .medium : .regular)
|
|
209
|
+
button.titleLabel?.numberOfLines = 1
|
|
210
|
+
button.titleLabel?.lineBreakMode = .byTruncatingTail
|
|
211
|
+
button.contentHorizontalAlignment = .center
|
|
212
|
+
button.contentVerticalAlignment = .center
|
|
213
|
+
button.rowTextOffsetY = 0
|
|
214
|
+
button.transform = .identity
|
|
215
|
+
button.marketLineHeight = nil
|
|
216
|
+
button.selectorSummaryLineHeight = nil
|
|
217
|
+
button.backgroundColor = .clear
|
|
218
|
+
button.pressedBackgroundColor = nil
|
|
219
|
+
button.layer.cornerRadius = 0
|
|
220
|
+
button.isHidden = true
|
|
221
|
+
button.isEnabled = true
|
|
222
|
+
button.alpha = 1
|
|
223
|
+
button.accessibilityIdentifier = nil
|
|
224
|
+
button.accessibilityLabel = nil
|
|
225
|
+
}
|
|
226
|
+
checkboxButton.isHidden = true
|
|
227
|
+
spinner.stopAnimating()
|
|
228
|
+
spinner.alpha = 1
|
|
229
|
+
}
|
|
230
|
+
@objc private func pressed(_ button: UIButton) {
|
|
231
|
+
guard accessoryActions.indices.contains(button.tag) else { return }
|
|
232
|
+
let action = accessoryActions[button.tag]
|
|
233
|
+
if !action.0.isEmpty { onAction?(action.0, button, button.tag, action.1) }
|
|
234
|
+
}
|
|
235
|
+
@objc private func checkboxPressed() {
|
|
236
|
+
if let action = checkboxAction { onAction?(action.0, checkboxButton, nil, action.1) }
|
|
237
|
+
}
|
|
238
|
+
private func bindAccessories(
|
|
239
|
+
_ item: NativeListItem,
|
|
240
|
+
_ accessories: [[String: Any]],
|
|
241
|
+
_ theme: [String: Any]?,
|
|
242
|
+
_ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
243
|
+
) {
|
|
244
|
+
var textIndex = 0
|
|
245
|
+
for accessory in accessories.prefix(2) {
|
|
246
|
+
switch accessory.string("kind") {
|
|
247
|
+
case "value":
|
|
248
|
+
showAccessory(textIndex, accessory.string("text"))
|
|
249
|
+
semanticValueButtons.append(accessoryButtons[textIndex])
|
|
250
|
+
if item.data.string("presentation") == "networkSelector" && item.hasExplicitHeight {
|
|
251
|
+
accessoryButtons[textIndex].contentHorizontalAlignment = .trailing
|
|
252
|
+
accessoryButtons[textIndex].titleLabel?.textAlignment = .right
|
|
253
|
+
}
|
|
254
|
+
applyValueSegments(
|
|
255
|
+
accessory.dictionaries("textSegments"), to: accessoryButtons[textIndex], theme: theme)
|
|
256
|
+
textIndex += 1
|
|
257
|
+
case "valuePair":
|
|
258
|
+
showValuePairAccessory(textIndex, accessory, theme: theme)
|
|
259
|
+
semanticValueButtons.append(accessoryButtons[textIndex])
|
|
260
|
+
textIndex += 1
|
|
261
|
+
case "checkbox": bindCheckbox(item, accessory, checkboxState)
|
|
262
|
+
case "radio":
|
|
263
|
+
showAccessory(
|
|
264
|
+
textIndex, accessory.bool("checked") ? "●" : "○", action: accessoryAction(accessory))
|
|
265
|
+
textIndex += 1
|
|
266
|
+
case "switch":
|
|
267
|
+
showAccessory(
|
|
268
|
+
textIndex, accessory.bool("value") ? "ON" : "OFF", action: accessoryAction(accessory))
|
|
269
|
+
textIndex += 1
|
|
270
|
+
case "chevron":
|
|
271
|
+
var icon = accessory
|
|
272
|
+
icon["name"] = "ChevronRightSmallOutline"
|
|
273
|
+
if icon["tintColor"] == nil {
|
|
274
|
+
icon["tintColor"] = theme?["iconSubdued"] as? String ?? "#8D8D8D"
|
|
275
|
+
}
|
|
276
|
+
if icon.string("actionKey").isEmpty { icon["actionKey"] = "press" }
|
|
277
|
+
showAccessoryIcon(textIndex, icon)
|
|
278
|
+
textIndex += 1
|
|
279
|
+
case "menu":
|
|
280
|
+
showMenuAccessory(textIndex, action: accessoryAction(accessory))
|
|
281
|
+
textIndex += 1
|
|
282
|
+
case "drag":
|
|
283
|
+
var icon = accessory
|
|
284
|
+
icon["name"] = "DragOutline"
|
|
285
|
+
if icon["tintColor"] == nil {
|
|
286
|
+
icon["tintColor"] = theme?["iconSubdued"] as? String ?? "#8D8D8D"
|
|
287
|
+
}
|
|
288
|
+
showAccessoryIcon(textIndex, icon)
|
|
289
|
+
textIndex += 1
|
|
290
|
+
case "icon":
|
|
291
|
+
var icon = accessory
|
|
292
|
+
if icon["tintColor"] == nil {
|
|
293
|
+
icon["tintColor"] = theme?["iconSubdued"] as? String ?? "#8D8D8D"
|
|
294
|
+
}
|
|
295
|
+
showAccessoryIcon(textIndex, icon)
|
|
296
|
+
textIndex += 1
|
|
297
|
+
case "spinner": spinner.startAnimating()
|
|
298
|
+
case "progress":
|
|
299
|
+
showAccessory(textIndex, "\(Int(accessory.double("value") * 100))%")
|
|
300
|
+
textIndex += 1
|
|
301
|
+
default: break
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private func bindCheckbox(
|
|
307
|
+
_ item: NativeListItem,
|
|
308
|
+
_ data: [String: Any],
|
|
309
|
+
_ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
310
|
+
) {
|
|
311
|
+
let targetData = data.dictionary("target")
|
|
312
|
+
let scope = targetData?.string("scope", default: "row") ?? "row"
|
|
313
|
+
let key =
|
|
314
|
+
scope == "section" ? targetData?.string("sectionKey") : scope == "row" ? item.key : nil
|
|
315
|
+
let target = NativeSelectionTarget(scope: scope, key: key)
|
|
316
|
+
let state = checkboxState(item, target, data.string("state", default: "unchecked"))
|
|
317
|
+
boundCheckboxData = data
|
|
318
|
+
boundCheckboxTarget = target
|
|
319
|
+
updateCheckboxPresentation(item, data, target: target, state: state)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private func updateCheckboxPresentation(
|
|
323
|
+
_ item: NativeListItem,
|
|
324
|
+
_ data: [String: Any],
|
|
325
|
+
target: NativeSelectionTarget,
|
|
326
|
+
state: String
|
|
327
|
+
) {
|
|
328
|
+
let accessoryDisabled = data.bool("disabled")
|
|
329
|
+
if data.bool("loading") {
|
|
330
|
+
checkboxButton.isHidden = true
|
|
331
|
+
spinner.startAnimating()
|
|
332
|
+
spinner.alpha = item.data.bool("disabled") ? 1 : accessoryDisabled ? 0.5 : 1
|
|
333
|
+
checkboxAction = nil
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
checkboxButton.isHidden = false
|
|
337
|
+
checkboxButton.backgroundColor =
|
|
338
|
+
state == "unchecked" ? checkboxUncheckedColor : checkboxCheckedColor
|
|
339
|
+
checkboxButton.layer.borderColor =
|
|
340
|
+
state == "unchecked"
|
|
341
|
+
? checkboxBorderColor.cgColor
|
|
342
|
+
: UIColor.clear.cgColor
|
|
343
|
+
let glyphName =
|
|
344
|
+
state == "indeterminate"
|
|
345
|
+
? "CheckboxIndeterminateCustom"
|
|
346
|
+
: "CheckboxCheckedCustom"
|
|
347
|
+
checkboxButton.setImage(
|
|
348
|
+
state == "unchecked" ? nil : nativeListIcon(named: glyphName),
|
|
349
|
+
for: .normal
|
|
350
|
+
)
|
|
351
|
+
checkboxButton.tintColor = checkboxIconColor
|
|
352
|
+
// A disabled ListItem already applies 0.5 to its complete content. Avoid
|
|
353
|
+
// multiplying that opacity on the nested control a second time.
|
|
354
|
+
checkboxButton.alpha = item.data.bool("disabled") ? 1 : accessoryDisabled ? 0.5 : 1
|
|
355
|
+
checkboxButton.isEnabled =
|
|
356
|
+
!item.data.bool("disabled") && !accessoryDisabled && !data.bool("loading")
|
|
357
|
+
checkboxAction = (data.string("actionKey", default: "selection"), target)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private func showAccessory(
|
|
361
|
+
_ index: Int,
|
|
362
|
+
_ value: String,
|
|
363
|
+
action: (String, NativeSelectionTarget?)? = nil,
|
|
364
|
+
color: UIColor? = nil
|
|
365
|
+
) {
|
|
366
|
+
guard accessoryButtons.indices.contains(index), !value.isEmpty else { return }
|
|
367
|
+
let button = accessoryButtons[index]
|
|
368
|
+
button.isHidden = false
|
|
369
|
+
button.setTitle(value, for: .normal)
|
|
370
|
+
button.setContentHuggingPriority(.required, for: .horizontal)
|
|
371
|
+
button.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
372
|
+
if let color { button.setTitleColor(color, for: .normal) }
|
|
373
|
+
while accessoryActions.count <= index { accessoryActions.append(("", nil)) }
|
|
374
|
+
accessoryActions[index] = action ?? ("", nil)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private func showValuePairAccessory(
|
|
378
|
+
_ index: Int,
|
|
379
|
+
_ accessory: [String: Any],
|
|
380
|
+
theme: [String: Any]?
|
|
381
|
+
) {
|
|
382
|
+
guard accessoryButtons.indices.contains(index) else { return }
|
|
383
|
+
let primary = accessory.string("primary")
|
|
384
|
+
let secondary = accessory.string("secondary")
|
|
385
|
+
guard !primary.isEmpty || !secondary.isEmpty else { return }
|
|
386
|
+
let button = accessoryButtons[index]
|
|
387
|
+
let text = NSMutableAttributedString()
|
|
388
|
+
let primaryParagraph = NSMutableParagraphStyle()
|
|
389
|
+
primaryParagraph.alignment = .right
|
|
390
|
+
primaryParagraph.minimumLineHeight = 20
|
|
391
|
+
primaryParagraph.maximumLineHeight = 20
|
|
392
|
+
text.append(
|
|
393
|
+
NSAttributedString(
|
|
394
|
+
string: primary,
|
|
395
|
+
attributes: [
|
|
396
|
+
.font: nativeListFont(ofSize: 16, weight: .medium),
|
|
397
|
+
.foregroundColor: accessoryTextColor(
|
|
398
|
+
accessory.string("primaryTone"),
|
|
399
|
+
defaultTone: "primary",
|
|
400
|
+
theme: theme
|
|
401
|
+
),
|
|
402
|
+
.paragraphStyle: primaryParagraph,
|
|
403
|
+
]
|
|
404
|
+
))
|
|
405
|
+
if !primary.isEmpty, !secondary.isEmpty { text.append(NSAttributedString(string: "\n")) }
|
|
406
|
+
let secondaryParagraph = NSMutableParagraphStyle()
|
|
407
|
+
secondaryParagraph.alignment = .right
|
|
408
|
+
secondaryParagraph.minimumLineHeight = 20
|
|
409
|
+
secondaryParagraph.maximumLineHeight = 20
|
|
410
|
+
text.append(
|
|
411
|
+
NSAttributedString(
|
|
412
|
+
string: secondary,
|
|
413
|
+
attributes: [
|
|
414
|
+
.font: nativeListFont(ofSize: 14),
|
|
415
|
+
.foregroundColor: accessoryTextColor(
|
|
416
|
+
accessory.string("secondaryTone"),
|
|
417
|
+
defaultTone: "secondary",
|
|
418
|
+
theme: theme
|
|
419
|
+
),
|
|
420
|
+
.paragraphStyle: secondaryParagraph,
|
|
421
|
+
]
|
|
422
|
+
))
|
|
423
|
+
button.isHidden = false
|
|
424
|
+
button.titleLabel?.numberOfLines = 2
|
|
425
|
+
button.contentHorizontalAlignment = .trailing
|
|
426
|
+
button.setAttributedTitle(text, for: .normal)
|
|
427
|
+
button.setContentHuggingPriority(.required, for: .horizontal)
|
|
428
|
+
button.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
429
|
+
while accessoryActions.count <= index { accessoryActions.append(("", nil)) }
|
|
430
|
+
accessoryActions[index] = ("", nil)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private func showMenuAccessory(
|
|
434
|
+
_ index: Int,
|
|
435
|
+
action: (String, NativeSelectionTarget?)?
|
|
436
|
+
) {
|
|
437
|
+
showAccessory(index, "⋮", action: action)
|
|
438
|
+
guard accessoryButtons.indices.contains(index) else { return }
|
|
439
|
+
let button = accessoryButtons[index]
|
|
440
|
+
button.titleLabel?.font = nativeListFont(ofSize: 20, weight: .medium)
|
|
441
|
+
accessorySizeConstraints.append(contentsOf: [
|
|
442
|
+
button.widthAnchor.constraint(equalToConstant: 24),
|
|
443
|
+
button.heightAnchor.constraint(equalToConstant: 24),
|
|
444
|
+
])
|
|
445
|
+
NSLayoutConstraint.activate(Array(accessorySizeConstraints.suffix(2)))
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private func accessoryTextColor(
|
|
449
|
+
_ tone: String,
|
|
450
|
+
defaultTone: String,
|
|
451
|
+
theme: [String: Any]?
|
|
452
|
+
) -> UIColor {
|
|
453
|
+
switch tone.isEmpty ? defaultTone : tone {
|
|
454
|
+
case "positive": return nativeListColor(theme, "positive", "#218358")
|
|
455
|
+
case "negative": return nativeListColor(theme, "negative", "#CE2C31")
|
|
456
|
+
// OneKey patch: account warnings and hidden balances use existing theme tokens.
|
|
457
|
+
case "disabled": return nativeListColor(theme, "disabledText", "#8D8D8D")
|
|
458
|
+
case "caution": return nativeListColor(theme, "caution", "#AB6400")
|
|
459
|
+
case "secondary": return nativeListColor(theme, "secondaryText", "#646464")
|
|
460
|
+
default: return nativeListColor(theme, "primaryText", "#202020")
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private func showAccessoryIcon(_ index: Int, _ data: [String: Any]) {
|
|
465
|
+
guard accessoryButtons.indices.contains(index) else { return }
|
|
466
|
+
let button = accessoryButtons[index]
|
|
467
|
+
button.isHidden = false
|
|
468
|
+
button.isEnabled = !data.bool("disabled")
|
|
469
|
+
button.alpha = button.isEnabled ? 1 : 0.4
|
|
470
|
+
let isAccountCreate =
|
|
471
|
+
currentItem?.data.string("presentation") == "accountSelector"
|
|
472
|
+
&& currentItem?.hasExplicitHeight == true && data.string("name") == "PlusSmallOutline"
|
|
473
|
+
let tintColor =
|
|
474
|
+
data["tintColor"] == nil && isAccountCreate
|
|
475
|
+
? nativeListColor(currentTheme, "iconSubdued", "#8D8D8D")
|
|
476
|
+
: UIColor(nativeListHex: data.string("tintColor", default: "#646464"), fallback: .darkGray)
|
|
477
|
+
button.tintColor = tintColor
|
|
478
|
+
if let image = nativeListIcon(named: data.string("name")) {
|
|
479
|
+
button.setImage(image, for: .normal)
|
|
480
|
+
button.setImage(
|
|
481
|
+
image.withTintColor(tintColor, renderingMode: .alwaysOriginal),
|
|
482
|
+
for: .disabled
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
let isDrillIn = data.string("kind") == "chevron"
|
|
486
|
+
let isAccountIcon = currentItem?.data.string("presentation") == "accountSelector" && !isDrillIn
|
|
487
|
+
let size: CGFloat = isDrillIn ? 24 : isAccountIcon && !isAccountCreate ? 38 : 36
|
|
488
|
+
if isAccountCreate { button.layer.cornerRadius = 8 }
|
|
489
|
+
if isAccountIcon && !isAccountCreate && !data.string("actionKey").isEmpty {
|
|
490
|
+
button.pressedBackgroundColor = nativeListColor(
|
|
491
|
+
currentTheme, "rowPressedBackground", "#E8E8E8")
|
|
492
|
+
button.layer.cornerRadius = size / 2
|
|
493
|
+
}
|
|
494
|
+
if isAccountIcon { contentGap = 5 }
|
|
495
|
+
button.accessibilityIdentifier = data["testID"] as? String
|
|
496
|
+
button.accessibilityLabel = data["accessibilityLabel"] as? String
|
|
497
|
+
if !isDrillIn, !data.string("actionKey").isEmpty {
|
|
498
|
+
// Reproduce the trailing edge of IconButton's m=-7 while keeping its
|
|
499
|
+
// full 36-point frame for padding/highlight behavior.
|
|
500
|
+
endInset = 5
|
|
501
|
+
}
|
|
502
|
+
accessorySizeConstraints.append(contentsOf: [
|
|
503
|
+
button.widthAnchor.constraint(equalToConstant: size),
|
|
504
|
+
button.heightAnchor.constraint(equalToConstant: size),
|
|
505
|
+
])
|
|
506
|
+
NSLayoutConstraint.activate(Array(accessorySizeConstraints.suffix(2)))
|
|
507
|
+
while accessoryActions.count <= index { accessoryActions.append(("", nil)) }
|
|
508
|
+
accessoryActions[index] =
|
|
509
|
+
data.bool("disabled")
|
|
510
|
+
? ("", nil)
|
|
511
|
+
: (data.string("actionKey"), nil)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private func accessoryAction(
|
|
515
|
+
_ accessory: [String: Any],
|
|
516
|
+
defaultKey: String = ""
|
|
517
|
+
) -> (String, NativeSelectionTarget?)? {
|
|
518
|
+
if accessory.bool("disabled") { return nil }
|
|
519
|
+
let key = accessory.string("actionKey", default: defaultKey)
|
|
520
|
+
return key.isEmpty ? nil : (key, nil)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private func applyValueSegments(
|
|
524
|
+
_ segments: [[String: Any]], to button: UIButton, theme: [String: Any]?
|
|
525
|
+
) {
|
|
526
|
+
guard !segments.isEmpty else { return }
|
|
527
|
+
let value = NSMutableAttributedString(string: "")
|
|
528
|
+
let color = nativeListColor(theme, "primaryText", "#202020")
|
|
529
|
+
// OneKey patch: rich currency changes font runs without dropping the established line baseline.
|
|
530
|
+
let current = button.attributedTitle(for: .normal)
|
|
531
|
+
var attributes =
|
|
532
|
+
current.flatMap { $0.length > 0 ? $0.attributes(at: 0, effectiveRange: nil) : nil } ?? [:]
|
|
533
|
+
attributes[.foregroundColor] = color
|
|
534
|
+
if currentItem?.data.string("presentation") == "networkSelector"
|
|
535
|
+
&& currentItem?.hasExplicitHeight == true
|
|
536
|
+
{
|
|
537
|
+
let paragraph =
|
|
538
|
+
(attributes[.paragraphStyle] as? NSParagraphStyle)?.mutableCopy()
|
|
539
|
+
as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
|
|
540
|
+
paragraph.alignment = .right
|
|
541
|
+
attributes[.paragraphStyle] = paragraph
|
|
542
|
+
}
|
|
543
|
+
for segment in segments {
|
|
544
|
+
attributes[.font] = nativeListTabularFont(
|
|
545
|
+
ofSize: segment.string("style") == "subscript" ? 10 : 16, weight: .medium)
|
|
546
|
+
value.append(NSAttributedString(string: segment.string("text"), attributes: attributes))
|
|
547
|
+
}
|
|
548
|
+
button.setAttributedTitle(value, for: .normal)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private func applyStyledButton(_ button: UIButton, _ style: [String: Any]) {
|
|
552
|
+
let text =
|
|
553
|
+
button.attributedTitle(for: .normal)?.string
|
|
554
|
+
?? button.title(for: .normal)
|
|
555
|
+
?? ""
|
|
556
|
+
guard !text.isEmpty, !style.isEmpty else { return }
|
|
557
|
+
let originalText = button.attributedTitle(for: .normal)
|
|
558
|
+
let accessory = button as? NativeListAccessoryButton
|
|
559
|
+
let baseFont =
|
|
560
|
+
(originalText?.attribute(.font, at: 0, effectiveRange: nil) as? UIFont) ?? button.titleLabel?
|
|
561
|
+
.font ?? nativeListFont(ofSize: 14)
|
|
562
|
+
let size = CGFloat(style.double("fontSize", default: Double(baseFont.pointSize)))
|
|
563
|
+
let font: UIFont
|
|
564
|
+
if let weightName = style["fontWeight"] as? String {
|
|
565
|
+
font = nativeListFont(
|
|
566
|
+
ofSize: size,
|
|
567
|
+
weight: marketFontWeight(weightName, fallback: .regular)
|
|
568
|
+
)
|
|
569
|
+
} else {
|
|
570
|
+
font = baseFont.withSize(size)
|
|
571
|
+
}
|
|
572
|
+
let fallbackColor = button.titleColor(for: .normal) ?? .black
|
|
573
|
+
let color =
|
|
574
|
+
(style["color"] as? String)
|
|
575
|
+
.map { UIColor(nativeListHex: $0, fallback: fallbackColor) }
|
|
576
|
+
?? fallbackColor
|
|
577
|
+
let attributed =
|
|
578
|
+
button.attributedTitle(for: .normal)
|
|
579
|
+
.map { NSMutableAttributedString(attributedString: $0) }
|
|
580
|
+
?? NSMutableAttributedString(string: text)
|
|
581
|
+
let paragraph =
|
|
582
|
+
(attributed.attribute(.paragraphStyle, at: 0, effectiveRange: nil)
|
|
583
|
+
as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
|
|
584
|
+
if let alignmentName = style["alignment"] as? String {
|
|
585
|
+
paragraph.alignment = marketTextAlignment(alignmentName)
|
|
586
|
+
button.contentHorizontalAlignment =
|
|
587
|
+
alignmentName == "start"
|
|
588
|
+
? .leading
|
|
589
|
+
: alignmentName == "end" ? .trailing : .center
|
|
590
|
+
}
|
|
591
|
+
var attributes: [NSAttributedString.Key: Any] = [:]
|
|
592
|
+
applyStyledFont(attributed, style, fallback: baseFont)
|
|
593
|
+
if style["color"] != nil { attributes[.foregroundColor] = color }
|
|
594
|
+
if style["alignment"] != nil || style["lineHeight"] != nil || style["lines"] != nil
|
|
595
|
+
|| style["truncate"] != nil
|
|
596
|
+
{
|
|
597
|
+
attributes[.paragraphStyle] = paragraph
|
|
598
|
+
}
|
|
599
|
+
if style["lineHeight"] != nil {
|
|
600
|
+
let box = CGFloat(style.double("lineHeight"))
|
|
601
|
+
paragraph.minimumLineHeight = box
|
|
602
|
+
paragraph.maximumLineHeight = box
|
|
603
|
+
attributes[.baselineOffset] = max(0, (box - font.lineHeight) / 2)
|
|
604
|
+
if accessory?.marketLineHeight != nil { accessory?.marketLineHeight = box }
|
|
605
|
+
if accessory?.selectorSummaryLineHeight != nil { accessory?.selectorSummaryLineHeight = box }
|
|
606
|
+
}
|
|
607
|
+
if style["lines"] != nil {
|
|
608
|
+
button.titleLabel?.numberOfLines = min(3, max(1, style.int("lines", default: 1)))
|
|
609
|
+
}
|
|
610
|
+
if style["lines"] != nil || style["truncate"] != nil {
|
|
611
|
+
let mode = styledLineBreakMode(style, lines: button.titleLabel?.numberOfLines ?? 1)
|
|
612
|
+
button.titleLabel?.lineBreakMode = mode
|
|
613
|
+
paragraph.lineBreakMode = mode
|
|
614
|
+
if button.titleLabel?.numberOfLines == 1 { normalizeSingleLine(attributed) }
|
|
615
|
+
}
|
|
616
|
+
if let alignment = style["verticalAlignment"] as? String {
|
|
617
|
+
button.contentVerticalAlignment =
|
|
618
|
+
alignment == "top" ? .top : alignment == "bottom" ? .bottom : .center
|
|
619
|
+
}
|
|
620
|
+
if style["offsetY"] != nil {
|
|
621
|
+
(button as? NativeListAccessoryButton)?.rowTextOffsetY = CGFloat(style.double("offsetY"))
|
|
622
|
+
}
|
|
623
|
+
button.titleLabel?.font = font
|
|
624
|
+
attributed.addAttributes(attributes, range: NSRange(location: 0, length: attributed.length))
|
|
625
|
+
button.setAttributedTitle(attributed, for: .normal)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private func applyStyledFont(
|
|
629
|
+
_ text: NSMutableAttributedString, _ style: [String: Any], fallback: UIFont
|
|
630
|
+
) {
|
|
631
|
+
guard style["fontSize"] != nil || style["fontWeight"] != nil else { return }
|
|
632
|
+
text.enumerateAttribute(.font, in: NSRange(location: 0, length: text.length)) {
|
|
633
|
+
value, range, _ in
|
|
634
|
+
let original = (value as? UIFont) ?? fallback
|
|
635
|
+
let size = CGFloat(style.double("fontSize", default: Double(original.pointSize)))
|
|
636
|
+
let font =
|
|
637
|
+
style["fontWeight"] == nil
|
|
638
|
+
? original.withSize(size)
|
|
639
|
+
: nativeListFont(
|
|
640
|
+
ofSize: size, weight: marketFontWeight(style.string("fontWeight"), fallback: .regular))
|
|
641
|
+
text.addAttribute(.font, value: font, range: range)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
private func normalizeSingleLine(_ text: NSMutableAttributedString) {
|
|
646
|
+
let pattern = try! NSRegularExpression(pattern: "\\r\\n|[\\r\\n]")
|
|
647
|
+
for match in pattern.matches(in: text.string, range: NSRange(location: 0, length: text.length))
|
|
648
|
+
.reversed()
|
|
649
|
+
{
|
|
650
|
+
text.replaceCharacters(in: match.range, with: " ")
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private func styledLineBreakMode(_ style: [String: Any], lines: Int) -> NSLineBreakMode {
|
|
655
|
+
if style.string("truncate", default: "tail") != "clip" { return .byTruncatingTail }
|
|
656
|
+
// Clipping disables wrapping; multi-line clip must wrap without adding an ellipsis.
|
|
657
|
+
return lines == 1 ? .byClipping : .byWordWrapping
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private func marketFontWeight(_ value: String, fallback: NativeListFontWeight)
|
|
661
|
+
-> NativeListFontWeight
|
|
662
|
+
{
|
|
663
|
+
switch value {
|
|
664
|
+
case "regular": return .regular
|
|
665
|
+
case "semibold": return .semibold
|
|
666
|
+
case "bold": return .bold
|
|
667
|
+
case "medium": return .medium
|
|
668
|
+
default: return fallback
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private func marketTextAlignment(_ value: String) -> NSTextAlignment {
|
|
673
|
+
switch value {
|
|
674
|
+
case "center": return .center
|
|
675
|
+
case "start": return effectiveUserInterfaceLayoutDirection == .rightToLeft ? .right : .left
|
|
676
|
+
case "end": return effectiveUserInterfaceLayoutDirection == .rightToLeft ? .left : .right
|
|
677
|
+
default: return .natural
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|