@onekeyfe/react-native-native-list 6.7.0-alpha.415 → 6.7.0-alpha.421

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 (121) hide show
  1. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +5 -0
  2. package/android/src/main/java/com/onekey/nativelist/NativeListGeneratedIcons.kt +47 -0
  3. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +5 -0
  4. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +120 -338
  5. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +5 -0
  6. package/docs/SPEC.md +139 -7
  7. package/ios/NativeListAccessoryStack.swift +6 -16
  8. package/ios/NativeListDataRowRenderer.swift +15 -6
  9. package/ios/NativeListDesignAssets.swift +13 -84
  10. package/ios/NativeListGeneratedIcons.swift +55 -0
  11. package/ios/NativeListIconBinding.swift +171 -0
  12. package/ios/NativeListIdentityRenderer.swift +12 -10
  13. package/ios/NativeListLeadingVisual.swift +11 -9
  14. package/ios/NativeListMarketRenderer.swift +7 -13
  15. package/ios/NativeListMediaTileRenderer.swift +6 -2
  16. package/ios/NativeListMetricCardRenderer.swift +4 -2
  17. package/ios/NativeListSectionHeaderRenderer.swift +12 -4
  18. package/ios/tests/NativeListAccessoryStackTests.swift +12 -0
  19. package/ios/tests/NativeListSharedIconTests.swift +236 -0
  20. package/lib/module/web/NativeListGeneratedIcons.js +31 -0
  21. package/lib/module/web/NativeListWebEngine.js +21 -323
  22. package/lib/module/web/templates/ActivityRowRenderer.js +4 -24
  23. package/lib/module/web/templates/MarketTableRowRenderer.js +36 -19
  24. package/lib/module/web/templates/MediaTileRowRenderer.js +2 -12
  25. package/lib/typescript/src/web/NativeListGeneratedIcons.d.ts +2 -0
  26. package/lib/typescript/src/web/NativeListWebEngine.d.ts +0 -1
  27. package/package.json +10 -4
  28. package/react-native-native-list.podspec +0 -5
  29. package/scripts/generate-icons.mjs +150 -0
  30. package/scripts/generate-icons.test.mjs +288 -0
  31. package/scripts/native-list-icons.json +260 -0
  32. package/src/web/NativeListGeneratedIcons.ts +28 -0
  33. package/src/web/NativeListWebEngine.ts +25 -405
  34. package/src/web/templates/ActivityRowRenderer.ts +15 -53
  35. package/src/web/templates/MarketTableRowRenderer.ts +53 -33
  36. package/src/web/templates/MediaTileRowRenderer.ts +2 -15
  37. package/ios/Resources/NativeListIcons.xcassets/Contents.json +0 -1
  38. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_bottom.imageset/Contents.json +0 -1
  39. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_bottom.imageset/icon.svg +0 -1
  40. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_top.imageset/Contents.json +0 -1
  41. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_top.imageset/icon.svg +0 -1
  42. package/ios/Resources/NativeListIcons.xcassets/onekey_badge_verified_solid.imageset/Contents.json +0 -1
  43. package/ios/Resources/NativeListIcons.xcassets/onekey_badge_verified_solid.imageset/icon.svg +0 -1
  44. package/ios/Resources/NativeListIcons.xcassets/onekey_chart_trending_up.imageset/Contents.json +0 -1
  45. package/ios/Resources/NativeListIcons.xcassets/onekey_chart_trending_up.imageset/icon.svg +0 -1
  46. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_checked.imageset/Contents.json +0 -1
  47. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_checked.imageset/icon.svg +0 -1
  48. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_indeterminate.imageset/Contents.json +0 -1
  49. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_indeterminate.imageset/icon.svg +0 -1
  50. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_bottom.imageset/Contents.json +0 -1
  51. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_bottom.imageset/icon.svg +0 -1
  52. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_grabber_ver.imageset/Contents.json +0 -1
  53. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_grabber_ver.imageset/icon.svg +0 -1
  54. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_right_small.imageset/Contents.json +0 -1
  55. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_right_small.imageset/icon.svg +0 -1
  56. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_top.imageset/Contents.json +0 -1
  57. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_top.imageset/icon.svg +0 -1
  58. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/Contents.json +0 -15
  59. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/icon.svg +0 -4
  60. package/ios/Resources/NativeListIcons.xcassets/onekey_document2.imageset/Contents.json +0 -1
  61. package/ios/Resources/NativeListIcons.xcassets/onekey_document2.imageset/icon.svg +0 -1
  62. package/ios/Resources/NativeListIcons.xcassets/onekey_dot_hor.imageset/Contents.json +0 -1
  63. package/ios/Resources/NativeListIcons.xcassets/onekey_dot_hor.imageset/icon.svg +0 -1
  64. package/ios/Resources/NativeListIcons.xcassets/onekey_dot_ver_solid.imageset/Contents.json +0 -1
  65. package/ios/Resources/NativeListIcons.xcassets/onekey_dot_ver_solid.imageset/icon.svg +0 -1
  66. package/ios/Resources/NativeListIcons.xcassets/onekey_drag.imageset/Contents.json +0 -1
  67. package/ios/Resources/NativeListIcons.xcassets/onekey_drag.imageset/icon.svg +0 -1
  68. package/ios/Resources/NativeListIcons.xcassets/onekey_error_solid.imageset/Contents.json +0 -1
  69. package/ios/Resources/NativeListIcons.xcassets/onekey_error_solid.imageset/icon.svg +0 -1
  70. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/Contents.json +0 -15
  71. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/icon.svg +0 -3
  72. package/ios/Resources/NativeListIcons.xcassets/onekey_image_square_waves.imageset/Contents.json +0 -1
  73. package/ios/Resources/NativeListIcons.xcassets/onekey_image_square_waves.imageset/icon.svg +0 -1
  74. package/ios/Resources/NativeListIcons.xcassets/onekey_info_circle.imageset/Contents.json +0 -1
  75. package/ios/Resources/NativeListIcons.xcassets/onekey_info_circle.imageset/icon.svg +0 -1
  76. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle.imageset/Contents.json +0 -1
  77. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle.imageset/icon.svg +0 -1
  78. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle_solid.imageset/Contents.json +0 -1
  79. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle_solid.imageset/icon.svg +0 -1
  80. package/ios/Resources/NativeListIcons.xcassets/onekey_pencil.imageset/Contents.json +0 -1
  81. package/ios/Resources/NativeListIcons.xcassets/onekey_pencil.imageset/icon.svg +0 -1
  82. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_circle.imageset/Contents.json +0 -1
  83. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_circle.imageset/icon.svg +0 -1
  84. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_large.imageset/Contents.json +0 -1
  85. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_large.imageset/icon.svg +0 -1
  86. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_small.imageset/Contents.json +0 -1
  87. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_small.imageset/icon.svg +0 -1
  88. package/ios/Resources/NativeListIcons.xcassets/onekey_questionmark_solid.imageset/Contents.json +0 -1
  89. package/ios/Resources/NativeListIcons.xcassets/onekey_questionmark_solid.imageset/icon.svg +0 -1
  90. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/Contents.json +0 -15
  91. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/icon.svg +0 -3
  92. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/Contents.json +0 -15
  93. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/icon.svg +0 -14
  94. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/Contents.json +0 -15
  95. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/icon.svg +0 -12
  96. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/Contents.json +0 -15
  97. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/icon.svg +0 -6
  98. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/Contents.json +0 -15
  99. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/icon.svg +0 -15
  100. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/Contents.json +0 -15
  101. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/icon.svg +0 -3
  102. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/Contents.json +0 -15
  103. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/icon.svg +0 -7
  104. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/Contents.json +0 -15
  105. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/icon.svg +0 -18
  106. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/Contents.json +0 -15
  107. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/icon.svg +0 -7
  108. package/ios/Resources/NativeListIcons.xcassets/onekey_shield_exclamation.imageset/Contents.json +0 -1
  109. package/ios/Resources/NativeListIcons.xcassets/onekey_shield_exclamation.imageset/icon.svg +0 -1
  110. package/ios/Resources/NativeListIcons.xcassets/onekey_speaker_promote.imageset/Contents.json +0 -1
  111. package/ios/Resources/NativeListIcons.xcassets/onekey_speaker_promote.imageset/icon.svg +0 -1
  112. package/ios/Resources/NativeListIcons.xcassets/onekey_star.imageset/Contents.json +0 -1
  113. package/ios/Resources/NativeListIcons.xcassets/onekey_star.imageset/icon.svg +0 -1
  114. package/ios/Resources/NativeListIcons.xcassets/onekey_star_solid.imageset/Contents.json +0 -1
  115. package/ios/Resources/NativeListIcons.xcassets/onekey_star_solid.imageset/icon.svg +0 -1
  116. package/ios/Resources/NativeListIcons.xcassets/onekey_swap_hor.imageset/Contents.json +0 -1
  117. package/ios/Resources/NativeListIcons.xcassets/onekey_swap_hor.imageset/icon.svg +0 -1
  118. package/ios/Resources/NativeListIcons.xcassets/onekey_ticket.imageset/Contents.json +0 -1
  119. package/ios/Resources/NativeListIcons.xcassets/onekey_ticket.imageset/icon.svg +0 -1
  120. package/ios/Resources/NativeListIcons.xcassets/onekey_wallet.imageset/Contents.json +0 -1
  121. package/ios/Resources/NativeListIcons.xcassets/onekey_wallet.imageset/icon.svg +0 -1
package/docs/SPEC.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # NativeList contract
2
2
 
3
+ ## Shared image SVG integration — Implemented
4
+
5
+ Static icons use the image package's named resources and SVG implementation:
6
+ the main registry, Activity's ten private glyphs, Market stars/recognized badge,
7
+ Media fallback and Android checkbox. The closed per-platform coverage, including
8
+ the main-line asset-list additions, is recorded under Built-in icon metadata
9
+ generation; unknown-name text fallbacks remain. The NativeList-only legacy name
10
+ `BadgeVerifiedSolid` resolves to canonical `BadgeRecognizedSolid`. Circle and
11
+ dynamic charts/spinners/dotted decoration remain local primitives.
12
+
13
+ Each iOS glyph slot owns a request owner and revision, including overlays and
14
+ UIButton states. Final graphical identity (name, raster geometry/scale and render
15
+ strategy) controls requests. A private synchronous bind transaction preserves
16
+ both an identical pending request and a ready raster across reset/rebind calls;
17
+ changed or empty identities cancel and clear. Only the current attached revision
18
+ may apply a completion. Detach, reuse and raster geometry changes invalidate
19
+ ownership. UIKit template tint changes reuse the immutable mask. UIButton states
20
+ share one decode, retaining template/original and disabled tint/alpha/action
21
+ behavior. Name validity and descriptor geometry control visibility, layout and
22
+ touch bounds; image availability does not control them.
23
+ Detached binds and auto-height measurement use only known intrinsic metadata;
24
+ they MUST NOT submit named requests or read/decode SVG resources. Initial attach
25
+ starts loading without changing the measured glyph or button dimensions.
26
+
27
+ iOS original brand/opacity modes and the size overload's always-template behavior
28
+ remain. Android named glyphs preserve their source aspect ratio, fit both source
29
+ dimensions within the view and optional glyph-size limit, and center the result
30
+ on both axes. This includes the non-square 16 × 20 Apple artwork; it MUST NOT
31
+ extend beyond the view. Checkboxes retain their 16 dp glyph plus 2 dp border
32
+ offset in selector geometry, otherwise 80 percent centered geometry. Shared images are immutable; no cross-view mutable
33
+ Drawable bounds or pixel changes are allowed. Named work uses image's existing
34
+ two-worker and bounded cache service. Web consumers use its pure DOM external-icon
35
+ helper while keeping inherited color, action labels, anchors and exact sizes.
36
+
37
+ Acceptance must cover actual slots, first bind and repeated row reuse, unknown
38
+ and empty inputs, detach/reattach, resize/tint changes, both UIButton states,
39
+ checkboxes, Apple, original brands, legacy badge, and Web/desktop menus. All
40
+ static path readers must be removed from migrated routes; procedural drawings
41
+ and unrelated URI image loading retain their existing ownership.
42
+
43
+ `NativeListRiskAccessoryTest` checks fractional and integral glyph bounds,
44
+ portrait/landscape sources, rectangular slots and both glyph-size scaling modes.
45
+ Its raw-SVG alpha comparison uses integral destination edges to isolate color
46
+ semantics from fractional bitmap interpolation.
47
+
3
48
  This is the entry point for the package's behavioral contract. The linked
4
49
  documents are part of this spec; implementation status is separate from target
5
50
  architecture and runtime evidence.
@@ -51,6 +96,55 @@ ordinary press for that gesture. Motion cancellation, row rebind/recycle,
51
96
  disabled state and disposal cancel pending recognition. Trailing accessory
52
97
  actions remain independent. Omission preserves existing tap behavior.
53
98
 
99
+ ### Native Swap token menu backport
100
+
101
+ Status: implemented in local native source, unreleased. Native build and runtime
102
+ acceptance are separate checks. Web and Desktop layout and actions stay unchanged;
103
+ their generated icon artwork is covered in the following section.
104
+
105
+ iOS and Android render `DotVerSolid` with the app's existing 24-unit solid icon
106
+ geometry. On Android, exactly two identity trailing accessories consisting of
107
+ `valuePair` followed by an `icon` named `DotVerSolid` with `actionKey: 'menu'`
108
+ lay out horizontally, matching iOS. Other icon names or action keys retain the
109
+ existing axis rules. This backport does not change row padding, accessory spacing,
110
+ button dimensions, actions, disabled state, or the public snapshot contract.
111
+
112
+ `NativeListAccessoryStackTests` checks the iOS image lookup and bound button image.
113
+ `NativeListRiskAccessoryTest` checks Android's actual icon drawing and the target
114
+ horizontal pair, including vertical controls on rebind.
115
+
116
+ ### Built-in icon metadata generation
117
+
118
+ The closed `scripts/native-list-icons.json` manifest retains platform coverage,
119
+ canonical App source paths and iOS rendering strategy. `Circle` is a local
120
+ primitive. The NativeList-only legacy `BadgeVerifiedSolid` name maps to
121
+ `solid/badge-recognized.svg` / canonical `BadgeRecognizedSolid`, preserving its
122
+ thumbs-up artwork without adding a general alias.
123
+
124
+ `scripts/generate-icons.mjs --app-root <app-root>` matches each source uniquely
125
+ against image's generated catalog, validates source digests and manifest
126
+ metadata, then emits exactly three small Swift/Kotlin/Web name/geometry tables.
127
+ It writes no SVG, path data or xcassets; image owns the single resource pipeline.
128
+ All validation precedes writes. `--check` is read-only and detects missing or
129
+ ambiguous sources, invalid metadata and stale output. Stable sorting emits no
130
+ machine paths or timestamps. Frozen tests compare exact registry key sets;
131
+ this does not prove that every application-computed name is registered.
132
+
133
+ The closed manifest contains 43 names with iOS 42, Android 41 and Web 25
134
+ registry entries (including DotVerSolid). Document2Outline, WalletOutline and
135
+ PlusLargeOutline are registered on all three platforms; TicketOutline also
136
+ retains Web and adds native coverage. These main-line names use image-owned
137
+ canonical SVGs, not duplicate NativeList paths or asset catalogs.
138
+ Existing Activity ten-name and Market/Media private call sites resolve canonical
139
+ names through the same DOM helper; their previous coverage does not expand.
140
+ Image preserves raw path order, fill rules, fixed colors, opacity and viewBoxes.
141
+ NativeList retains its layout, button bounds, actions and snapshot interfaces.
142
+
143
+ Focused SDK fixture acceptance covers native attached lifecycle/rebinding,
144
+ legacy iOS asset comparisons, shared alpha semantics and real Web/Electron
145
+ source/lib consumers. This is not full App, balance, transaction or signed
146
+ release acceptance.
147
+
54
148
  Identity trailing accessories stack vertically, except for the pairs every
55
149
  platform lays out side by side: `value` + `checkbox`, `valuePair` + `menu`,
56
150
  `valuePair` + an `icon` named `DotVerSolid` whose `actionKey` is `menu`,
@@ -61,13 +155,6 @@ platform lays out side by side: `value` + `checkbox`, `valuePair` + `menu`,
61
155
  pair is the Swap token selector's balance column and row menu; stacking it
62
156
  clipped both inside the fixed 60dp row on Android.
63
157
 
64
- Icon names are a closed set drawn from the app's 24-unit Icon sources on every
65
- platform (`iconPaths` on Android, `NativeListIcons.xcassets` on iOS,
66
- `selectorIcons` on Web); an unmapped name renders an empty slot rather than a
67
- placeholder. The set includes every name the app's NativeList builders pass,
68
- including `DotVerSolid`, `Document2Outline`, `WalletOutline`, `PlusLargeOutline`
69
- and `TicketOutline` (added 2026-10-30 after the OK-66072 audit found them missing
70
- on native).
71
158
 
72
159
  A `message` row may opt into `bodyLines: 0`; its complete body wraps within its
73
160
  existing text area and determines natural height unless an explicit row or
@@ -1941,6 +2028,23 @@ logo groups retain their4px gap and overlap geometry. No other text-part gaps
1941
2028
  change. Regression acceptance includes Dark/Light recycled visuals, preserved
1942
2029
  network overlays, independent copy actions and Stock hover group spacing.
1943
2030
 
2031
+ For token visuals in Web/Electron Market tables, changing only `overlays` MUST
2032
+ retain the main image frame, its loaded/error visibility and pending image
2033
+ callbacks. Removed overlay images release their pending work. Changes to the
2034
+ main image descriptor or other leading-visual fields keep the existing full
2035
+ rebind behavior. This is a Web renderer lifecycle rule; it changes neither the
2036
+ snapshot schema nor the iOS/Android implementations. Acceptance covers adding,
2037
+ replacing and removing overlays before and after image load/failure, including
2038
+ consumer row-key changes, and replacing the main image source. Overlay-only
2039
+ updates MUST retain the existing circular bitmap clipping of padded square
2040
+ badges and their caller-provided geometry.
2041
+ Status: implemented on the 6.7.0-alpha.402 publication source. Package DOM
2042
+ regressions, the built-module Chromium lifecycle fixture and consuming Electron
2043
+ refresh/navigation badge rendering are verified. Consuming Electron acceptance
2044
+ also covers a manually locked refresh, user password unlock, first Market entry
2045
+ and a complete quote polling cycle. No native implementation or public API
2046
+ changes are included.
2047
+
1944
2048
  Gallery collection subtitles retain an 8px trailing text inset whether or not
1945
2049
  a network thumbnail is present. The subtitle uses border-box sizing and its row
1946
2050
  has no extra gap, preserving the same 8px separation with a thumbnail and the
@@ -2036,3 +2140,31 @@ A containing Web pager therefore applies insets, collapse/top commands and route
2036
2140
  restoration to the owned host, with slot content as its padding target. The
2037
2141
  ordinary no-slot viewport and explicit external-host paths keep their existing
2038
2142
  ownership; no Pager dependency or new API is required.
2143
+
2144
+ ### Shared icon recoloring revision — Implemented; scoped runtime acceptance
2145
+
2146
+ Android named glyph slots consume image's typed immutable shape-cache results:
2147
+ compatible fixed/single/same-alpha multiple colors update per-view tint without a
2148
+ request; new alpha or mixed-color raster requests preserve the last successful
2149
+ same-source/geometry bitmap until completion. Name/geometry changes, invalid,
2150
+ hidden/detach/recycle clear immediately and revisions reject old callbacks.
2151
+ Circle remains local; checkbox rectangles and icon coverage are unchanged.
2152
+ Android non-square named glyphs use the aspect-fit geometry defined above.
2153
+ iOS retains its existing black template/original request and synchronous retint.
2154
+ Web React and DOM use the image package's demand-loaded external per-icon symbol
2155
+ URLs, with existing coverage/aliases/action labels and CSS color inheritance.
2156
+
2157
+ ## Host-owned named resources — Implemented; SDK fixture runtime verified
2158
+
2159
+ Image provides decoding and the static Web `#onekey-icon-assets` provider boundary;
2160
+ the application owns complete named SVG resources. NativeList keeps its existing
2161
+ 43-name platform coverage and three generated metadata tables, including its
2162
+ legacy `BadgeVerifiedSolid` to canonical `BadgeRecognizedSolid` mapping. No list
2163
+ props, naming coverage or rendering strategy changes. Android non-square glyph
2164
+ geometry follows the aspect-fit contract above.
2165
+
2166
+ The SDK metadata generator validates against the shared 63-sample example/test
2167
+ catalog without an App checkout, including raw source digests and viewBox sizes.
2168
+ It emits metadata only. Production consumers must supply the complete host icon
2169
+ bundle/assets and Web provider; SDK-only fixture tests prove their closed sample
2170
+ coverage, while the App's independent generator verifies all 1,909 resources.
@@ -1,7 +1,7 @@
1
1
  import UIKit
2
2
 
3
3
  // OneKey patch: explicit summary actions use the source text's physical-pixel line box.
4
- final class NativeListAccessoryButton: UIButton {
4
+ final class NativeListAccessoryButton: NativeListIconButton {
5
5
  var pressedBackgroundColor: UIColor?
6
6
  var normalBackgroundColor: UIColor = .clear
7
7
  var fixedTextLineHeight: CGFloat?
@@ -197,7 +197,7 @@ final class NativeListAccessoryButton: UIButton {
197
197
  final class NativeListAccessoryStack: UIStackView {
198
198
  var fonts = NativeListFontFamilies()
199
199
  private let accessoryButtons = (0..<2).map { _ in NativeListAccessoryButton(type: .system) }
200
- private let checkboxButton = UIButton(type: .system)
200
+ private let checkboxButton = NativeListIconButton(type: .system)
201
201
  private let spinner = UIActivityIndicatorView(style: .medium)
202
202
  private var accessoryActions: [(String, NativeSelectionTarget?)] = []
203
203
  private var checkboxAction: (String, NativeSelectionTarget?)?
@@ -250,6 +250,8 @@ final class NativeListAccessoryStack: UIStackView {
250
250
  checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
251
251
  fonts: NativeListFontFamilies
252
252
  ) {
253
+ let iconUpdate = NativeListIconUpdate(self)
254
+ defer { iconUpdate.finish() }
253
255
  self.fonts = fonts
254
256
  reset()
255
257
  currentItem = item
@@ -501,10 +503,7 @@ final class NativeListAccessoryStack: UIStackView {
501
503
  state == "indeterminate"
502
504
  ? "CheckboxIndeterminateCustom"
503
505
  : "CheckboxCheckedCustom"
504
- checkboxButton.setImage(
505
- state == "unchecked" ? nil : nativeListIcon(named: glyphName),
506
- for: .normal
507
- )
506
+ checkboxButton.bindIcon(state == "unchecked" ? nil : glyphName)
508
507
  checkboxButton.tintColor = checkboxIconColor
509
508
  // A disabled ListItem already applies 0.5 to its complete content. Avoid
510
509
  // multiplying that opacity on the nested control a second time.
@@ -666,16 +665,7 @@ final class NativeListAccessoryStack: UIStackView {
666
665
  button.tintColor = tintColor
667
666
  let isDecorativeRefresh =
668
667
  data.string("name") == "RefreshCcwOutline" && data.string("actionKey").isEmpty
669
- let image = isDecorativeRefresh
670
- ? nativeListIcon(named: data.string("name"), size: CGSize(width: 16, height: 16))
671
- : nativeListIcon(named: data.string("name"))
672
- if let image {
673
- button.setImage(image, for: .normal)
674
- button.setImage(
675
- image.withTintColor(tintColor, renderingMode: .alwaysOriginal),
676
- for: .disabled
677
- )
678
- }
668
+ button.bindIcon(data.string("name"), size: isDecorativeRefresh ? CGSize(width: 16, height: 16) : nil)
679
669
  let isDrillIn = data.string("kind") == "chevron"
680
670
  let isTertiary = data.string("buttonStyle") == "tertiary"
681
671
  if data.string("kind") == "icon" && isTertiary {
@@ -12,7 +12,7 @@ final class NativeListDataRowCell: NativeListRendererCell {
12
12
  }
13
13
 
14
14
  private let visual = NativeListLeadingVisual()
15
- private let favorite = UIImageView()
15
+ private let favorite = NativeListIconImageView()
16
16
  private let accessories = NativeListAccessoryStack()
17
17
  private let trailingAccessories = NativeListAccessoryStack()
18
18
  private let columns = UIStackView()
@@ -51,6 +51,9 @@ final class NativeListDataRowCell: NativeListRendererCell {
51
51
  _ item: NativeListItem, theme: [String: Any]?, layout: String,
52
52
  checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
53
53
  ) {
54
+ let iconUpdate = NativeListIconUpdate(self)
55
+ defer { iconUpdate.finish() }
56
+
54
57
  let data = item.data
55
58
  let style = data.dictionary("style") ?? [:]
56
59
  let image = style.dictionary("image") ?? [:]
@@ -64,8 +67,7 @@ final class NativeListDataRowCell: NativeListRendererCell {
64
67
  let vp = CGFloat(style.double("verticalPadding", default: layout == "table" ? 10 : 8))
65
68
  contentInsets = UIEdgeInsets(top: vp, left: hp, bottom: vp, right: hp)
66
69
  favorite.isHidden = !data.bool("favorite") && !data.bool("favoriteActive")
67
- favorite.image = nativeListIcon(
68
- named: data.bool("favoriteActive") ? "StarSolid" : "StarOutline")
70
+ favorite.bindIcon(data.bool("favoriteActive") ? "StarSolid" : "StarOutline")
69
71
  favorite.tintColor = nativeListColor(
70
72
  theme, data.bool("favoriteActive") ? "icon" : "iconSubdued",
71
73
  data.bool("favoriteActive") ? "#646464" : "#8D8D8D")
@@ -166,13 +168,18 @@ final class NativeListDataRowCell: NativeListRendererCell {
166
168
  NSLayoutConstraint.activate(columnWidthConstraints)
167
169
  columnWeights = normalizedWeights
168
170
  }
171
+
169
172
  }
170
173
  override func bindSelectionContent(
171
174
  _ item: NativeListItem,
172
175
  checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
173
176
  ) {
177
+ let iconUpdate = NativeListIconUpdate(self)
178
+ defer { iconUpdate.finish() }
179
+
174
180
  accessories.updateSelection(item, checkboxState: checkboxState)
175
181
  trailingAccessories.updateSelection(item, checkboxState: checkboxState)
182
+
176
183
  }
177
184
  override func recycleContent() {
178
185
  visual.recycle()
@@ -185,8 +192,8 @@ final class NativeListDataRowCell: NativeListRendererCell {
185
192
  private final class NativeListTableColumnView: UIStackView {
186
193
  private let primaryLine = UIStackView()
187
194
  private let primaryLabel = NativeListTextLabel()
188
- private let titleIconView = UIImageView()
189
- private let secondaryTitleIconView = UIImageView()
195
+ private let titleIconView = NativeListIconImageView()
196
+ private let secondaryTitleIconView = NativeListIconImageView()
190
197
  private let badgesStack = UIStackView()
191
198
  private let secondaryLine = UIStackView()
192
199
  private let secondaryLeadingLabel = NativeListTextLabel()
@@ -280,7 +287,7 @@ private final class NativeListTableColumnView: UIStackView {
280
287
  for (index, descriptor) in descriptors.enumerated() {
281
288
  guard let descriptor else { continue }
282
289
  let icon = index == 0 ? titleIconView : secondaryTitleIconView
283
- icon.image = nativeListIcon(named: descriptor.string("name"))
290
+ icon.bindIcon(descriptor.string("name"))
284
291
  icon.tintColor = UIColor(
285
292
  nativeListHex: descriptor.string("tintColor",
286
293
  default: theme?["iconSubdued"] as? String ?? "#8D8D8D"),
@@ -303,6 +310,8 @@ private final class NativeListTableColumnView: UIStackView {
303
310
  theme: [String: Any]?,
304
311
  fonts: NativeListFontFamilies
305
312
  ) {
313
+ let iconUpdate = NativeListIconUpdate(self)
314
+ defer { iconUpdate.finish() }
306
315
  reset()
307
316
  let textAlignment: NSTextAlignment
308
317
  switch column.string("alignment") {
@@ -1,8 +1,7 @@
1
1
  import CoreText
2
+ import OneKeyImage
2
3
  import UIKit
3
4
 
4
- private final class NativeListResourceToken {}
5
-
6
5
  enum NativeListFontWeight {
7
6
  case regular
8
7
  case medium
@@ -19,19 +18,6 @@ enum NativeListFontWeight {
19
18
  }
20
19
  }
21
20
 
22
- private enum NativeListResources {
23
- static let bundle: Bundle = {
24
- let owners = [Bundle(for: NativeListResourceToken.self), .main]
25
- for owner in owners {
26
- if let url = owner.url(forResource: "NativeListResources", withExtension: "bundle"),
27
- let bundle = Bundle(url: url) {
28
- return bundle
29
- }
30
- }
31
- return Bundle(for: NativeListResourceToken.self)
32
- }()
33
- }
34
-
35
21
  struct NativeListFontFamilies: Equatable {
36
22
  let names: [String: String]
37
23
  init(names: [String: String] = [:]) { self.names = names }
@@ -84,76 +70,19 @@ func nativeListTabularFont(
84
70
  descriptor: font.fontDescriptor.addingAttributes([.featureSettings: settings]), size: size)
85
71
  }
86
72
 
87
- func nativeListIcon(named name: String) -> UIImage? {
88
- // OneKey patch: the connection indicator is a solid circle at its requested size.
73
+ func nativeListIconMetadata(named name: String) -> NativeListIconMetadata? {
89
74
  if name == "Circle" {
90
- return UIGraphicsImageRenderer(size: CGSize(width: 24, height: 24)).image { _ in
91
- UIColor.black.setFill()
92
- UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 24, height: 24)).fill()
93
- }.withRenderingMode(.alwaysTemplate)
75
+ return NativeListIconMetadata(name: "Circle", size: CGSize(width: 24, height: 24), original: false)
94
76
  }
95
- let assetName: String
96
- switch name {
97
- // OneKey patch: official selector icon geometry.
98
- case "AccountErrorCustom": assetName = "onekey_selector_account_error_custom"
99
- // OneKey patch: official selector icon geometry.
100
- case "CrossedSmallSolid": assetName = "onekey_selector_crossed_small_solid"
101
- // OneKey patch: official selector icon geometry.
102
- case "AllNetworksSolid": assetName = "onekey_selector_all_networks_solid"
103
- // OneKey patch: official selector icon geometry.
104
- case "BotIllus": assetName = "onekey_selector_bot_illus"
105
- // OneKey patch: official selector icon geometry.
106
- case "AppleBrand": assetName = "onekey_selector_apple_brand"
107
- // OneKey patch: official selector icon geometry.
108
- case "GoogleIllus": assetName = "onekey_selector_google_illus"
109
- // OneKey patch: official selector icon geometry.
110
- case "LockSolid": assetName = "onekey_selector_lock_solid"
111
- // OneKey patch: official selector icon geometry.
112
- case "GlobusOutline": assetName = "onekey_selector_globus_outline"
113
- case "GasSolid": assetName = "onekey_gas_solid"
114
- case "RefreshCcwOutline": assetName = "onekey_refresh_ccw_outline"
115
- case "CryptoCoinOutline": assetName = "onekey_crypto_coin_outline"
116
- case "ArrowBottomOutline": assetName = "onekey_arrow_bottom"
117
- case "ArrowTopOutline": assetName = "onekey_arrow_top"
118
- case "ChartTrendingUpOutline": assetName = "onekey_chart_trending_up"
119
- case "SwapHorOutline": assetName = "onekey_swap_hor"
120
- case "ShieldExclamationOutline": assetName = "onekey_shield_exclamation"
121
- case "InfoCircleOutline": assetName = "onekey_info_circle"
122
- case "SpeakerPromoteOutline": assetName = "onekey_speaker_promote"
123
- case "PlusCircleOutline": assetName = "onekey_plus_circle"
124
- case "PlusSmallOutline": assetName = "onekey_plus_small"
125
- case "DotHorOutline": assetName = "onekey_dot_hor"
126
- case "PencilOutline": assetName = "onekey_pencil"
127
- case "MinusCircleSolid": assetName = "onekey_minus_circle_solid"
128
- case "MinusCircleOutline": assetName = "onekey_minus_circle"
129
- case "ChevronRightSmallOutline": assetName = "onekey_chevron_right_small"
130
- case "DragOutline": assetName = "onekey_drag"
131
- case "StarOutline": assetName = "onekey_star"
132
- case "StarSolid": assetName = "onekey_star_solid"
133
- case "BadgeVerifiedSolid": assetName = "onekey_badge_verified_solid"
134
- case "ChevronGrabberVerOutline": assetName = "onekey_chevron_grabber_ver"
135
- case "ChevronBottomOutline": assetName = "onekey_chevron_bottom"
136
- case "ChevronTopOutline": assetName = "onekey_chevron_top"
137
- case "CheckboxCheckedCustom": assetName = "onekey_checkbox_checked"
138
- case "CheckboxIndeterminateCustom": assetName = "onekey_checkbox_indeterminate"
139
- case "ErrorSolid": assetName = "onekey_error_solid"
140
- case "QuestionmarkSolid": assetName = "onekey_questionmark_solid"
141
- case "ImageSquareWavesOutline": assetName = "onekey_image_square_waves"
142
- // Asset-list icons the app passes by name (OK-66072 audit): unmapped names rendered nothing.
143
- case "DotVerSolid": assetName = "onekey_dot_ver_solid"
144
- case "Document2Outline": assetName = "onekey_document2"
145
- case "WalletOutline": assetName = "onekey_wallet"
146
- case "PlusLargeOutline": assetName = "onekey_plus_large"
147
- case "TicketOutline": assetName = "onekey_ticket"
148
- default: return nil
77
+ if let generated = NativeListGeneratedIcons.icons[name] {
78
+ return generated
149
79
  }
150
- return UIImage(named: assetName, in: NativeListResources.bundle, compatibleWith: nil)?
151
- .withRenderingMode(["GoogleIllus", "BotIllus", "AccountErrorCustom"].contains(name) ? .alwaysOriginal : .alwaysTemplate)
152
- }
153
-
154
- func nativeListIcon(named name: String, size: CGSize) -> UIImage? {
155
- guard let image = nativeListIcon(named: name) else { return nil }
156
- return UIGraphicsImageRenderer(size: size).image { _ in
157
- image.draw(in: CGRect(origin: .zero, size: size))
158
- }.withRenderingMode(.alwaysTemplate)
80
+ guard let meta = OneKeyNamedSVG.shared.iconMetadata(forName: name) else {
81
+ return nil
82
+ }
83
+ return NativeListIconMetadata(
84
+ name: name,
85
+ size: CGSize(width: meta.width, height: meta.height),
86
+ original: meta.recolorMode == .fixed || meta.recolorMode == .raster
87
+ )
159
88
  }
@@ -0,0 +1,55 @@
1
+ // Generated by scripts/generate-icons.mjs. Coverage and legacy names only; hosts provide SVG resources.
2
+ import UIKit
3
+
4
+ struct NativeListIconMetadata {
5
+ let name: String
6
+ let size: CGSize
7
+ let original: Bool
8
+ }
9
+
10
+ enum NativeListGeneratedIcons {
11
+ static let icons: [String: NativeListIconMetadata] = [
12
+ "AccountErrorCustom": NativeListIconMetadata(name: "AccountErrorCustom", size: CGSize(width: 18, height: 18), original: true),
13
+ "AllNetworksSolid": NativeListIconMetadata(name: "AllNetworksSolid", size: CGSize(width: 24, height: 24), original: false),
14
+ "AppleBrand": NativeListIconMetadata(name: "AppleBrand", size: CGSize(width: 16, height: 20), original: false),
15
+ "ArrowBottomOutline": NativeListIconMetadata(name: "ArrowBottomOutline", size: CGSize(width: 24, height: 24), original: false),
16
+ "ArrowTopOutline": NativeListIconMetadata(name: "ArrowTopOutline", size: CGSize(width: 24, height: 24), original: false),
17
+ "BadgeVerifiedSolid": NativeListIconMetadata(name: "BadgeRecognizedSolid", size: CGSize(width: 24, height: 24), original: false),
18
+ "BotIllus": NativeListIconMetadata(name: "BotIllus", size: CGSize(width: 24, height: 24), original: true),
19
+ "ChartTrendingUpOutline": NativeListIconMetadata(name: "ChartTrendingUpOutline", size: CGSize(width: 24, height: 24), original: false),
20
+ "CheckboxCheckedCustom": NativeListIconMetadata(name: "CheckboxCheckedCustom", size: CGSize(width: 16, height: 16), original: false),
21
+ "CheckboxIndeterminateCustom": NativeListIconMetadata(name: "CheckboxIndeterminateCustom", size: CGSize(width: 16, height: 16), original: false),
22
+ "ChevronBottomOutline": NativeListIconMetadata(name: "ChevronBottomOutline", size: CGSize(width: 24, height: 24), original: false),
23
+ "ChevronGrabberVerOutline": NativeListIconMetadata(name: "ChevronGrabberVerOutline", size: CGSize(width: 24, height: 24), original: false),
24
+ "ChevronRightSmallOutline": NativeListIconMetadata(name: "ChevronRightSmallOutline", size: CGSize(width: 24, height: 24), original: false),
25
+ "ChevronTopOutline": NativeListIconMetadata(name: "ChevronTopOutline", size: CGSize(width: 24, height: 24), original: false),
26
+ "CrossedSmallSolid": NativeListIconMetadata(name: "CrossedSmallSolid", size: CGSize(width: 24, height: 24), original: false),
27
+ "CryptoCoinOutline": NativeListIconMetadata(name: "CryptoCoinOutline", size: CGSize(width: 24, height: 24), original: false),
28
+ "Document2Outline": NativeListIconMetadata(name: "Document2Outline", size: CGSize(width: 24, height: 24), original: false),
29
+ "DotHorOutline": NativeListIconMetadata(name: "DotHorOutline", size: CGSize(width: 24, height: 24), original: false),
30
+ "DotVerSolid": NativeListIconMetadata(name: "DotVerSolid", size: CGSize(width: 24, height: 24), original: false),
31
+ "DragOutline": NativeListIconMetadata(name: "DragOutline", size: CGSize(width: 24, height: 24), original: false),
32
+ "ErrorSolid": NativeListIconMetadata(name: "ErrorSolid", size: CGSize(width: 24, height: 24), original: false),
33
+ "GasSolid": NativeListIconMetadata(name: "GasSolid", size: CGSize(width: 24, height: 24), original: false),
34
+ "GlobusOutline": NativeListIconMetadata(name: "GlobusOutline", size: CGSize(width: 24, height: 24), original: false),
35
+ "GoogleIllus": NativeListIconMetadata(name: "GoogleIllus", size: CGSize(width: 24, height: 24), original: true),
36
+ "ImageSquareWavesOutline": NativeListIconMetadata(name: "ImageSquareWavesOutline", size: CGSize(width: 24, height: 24), original: false),
37
+ "InfoCircleOutline": NativeListIconMetadata(name: "InfoCircleOutline", size: CGSize(width: 24, height: 24), original: false),
38
+ "LockSolid": NativeListIconMetadata(name: "LockSolid", size: CGSize(width: 24, height: 24), original: false),
39
+ "MinusCircleOutline": NativeListIconMetadata(name: "MinusCircleOutline", size: CGSize(width: 24, height: 24), original: false),
40
+ "MinusCircleSolid": NativeListIconMetadata(name: "MinusCircleSolid", size: CGSize(width: 24, height: 24), original: false),
41
+ "PencilOutline": NativeListIconMetadata(name: "PencilOutline", size: CGSize(width: 24, height: 24), original: false),
42
+ "PlusCircleOutline": NativeListIconMetadata(name: "PlusCircleOutline", size: CGSize(width: 24, height: 24), original: false),
43
+ "PlusLargeOutline": NativeListIconMetadata(name: "PlusLargeOutline", size: CGSize(width: 24, height: 24), original: false),
44
+ "PlusSmallOutline": NativeListIconMetadata(name: "PlusSmallOutline", size: CGSize(width: 24, height: 24), original: false),
45
+ "QuestionmarkSolid": NativeListIconMetadata(name: "QuestionmarkSolid", size: CGSize(width: 24, height: 24), original: false),
46
+ "RefreshCcwOutline": NativeListIconMetadata(name: "RefreshCcwOutline", size: CGSize(width: 24, height: 24), original: false),
47
+ "ShieldExclamationOutline": NativeListIconMetadata(name: "ShieldExclamationOutline", size: CGSize(width: 24, height: 24), original: false),
48
+ "SpeakerPromoteOutline": NativeListIconMetadata(name: "SpeakerPromoteOutline", size: CGSize(width: 24, height: 24), original: false),
49
+ "StarOutline": NativeListIconMetadata(name: "StarOutline", size: CGSize(width: 24, height: 24), original: false),
50
+ "StarSolid": NativeListIconMetadata(name: "StarSolid", size: CGSize(width: 24, height: 24), original: false),
51
+ "SwapHorOutline": NativeListIconMetadata(name: "SwapHorOutline", size: CGSize(width: 24, height: 24), original: false),
52
+ "TicketOutline": NativeListIconMetadata(name: "TicketOutline", size: CGSize(width: 24, height: 24), original: false),
53
+ "WalletOutline": NativeListIconMetadata(name: "WalletOutline", size: CGSize(width: 24, height: 24), original: false),
54
+ ]
55
+ }
@@ -0,0 +1,171 @@
1
+ import OneKeyImage
2
+ import UIKit
3
+
4
+ /// One request owner per glyph slot; metadata also supplies detached measurement.
5
+ private final class NativeListIconBinding {
6
+ private weak var view: UIView?
7
+ private let owner = NSObject()
8
+ private let apply: (UIImage?) -> Void
9
+ private var descriptor: NativeListIconMetadata?
10
+ private var pointSize = CGSize.zero
11
+ private var template = true
12
+ private var sizeOverride: CGSize?
13
+ private var currentImage: UIImage?
14
+ private var placeholder: UIImage?
15
+ private var updateDepth = 0
16
+ private var pendingClear = false
17
+ private var revision: UInt64 = 0
18
+ private var identity: String?
19
+
20
+ init(view: UIView, apply: @escaping (UIImage?) -> Void) { self.view = view; self.apply = apply }
21
+
22
+ func bind(_ name: String?, size: CGSize? = nil) {
23
+ guard let name, let metadata = nativeListIconMetadata(named: name) else {
24
+ clearNow(); return
25
+ }
26
+ if descriptor?.name == metadata.name, descriptor?.original == metadata.original,
27
+ descriptor?.size == metadata.size, sizeOverride == size {
28
+ pendingClear = false
29
+ apply(currentImage)
30
+ refresh(); return
31
+ }
32
+ clearNow()
33
+ descriptor = metadata
34
+ pointSize = size ?? metadata.size
35
+ sizeOverride = size
36
+ template = size != nil || !metadata.original
37
+ let format = UIGraphicsImageRendererFormat(); format.scale = 1
38
+ placeholder = UIGraphicsImageRenderer(size: pointSize, format: format).image { _ in }
39
+ .withRenderingMode(template ? .alwaysTemplate : .alwaysOriginal)
40
+ currentImage = placeholder
41
+ apply(currentImage)
42
+ refresh()
43
+ }
44
+
45
+ func clear(display: Bool = true) {
46
+ if display, updateDepth > 0 {
47
+ pendingClear = true; apply(nil); return
48
+ }
49
+ clearNow(display: display)
50
+ }
51
+
52
+ private func clearNow(display: Bool = true) {
53
+ revision &+= 1; identity = nil; descriptor = nil; currentImage = nil; placeholder = nil
54
+ pendingClear = false
55
+ OneKeyNamedSVG.shared.cancel(owner: owner)
56
+ if display { apply(nil) }
57
+ }
58
+
59
+ func beginUpdate() { updateDepth += 1 }
60
+ func endUpdate() {
61
+ updateDepth -= 1
62
+ guard updateDepth == 0 else { return }
63
+ if pendingClear { clearNow() } else { refresh() }
64
+ }
65
+
66
+ func retint() { if descriptor != nil && !pendingClear { apply(currentImage) } }
67
+
68
+ func refresh(force: Bool = false) {
69
+ guard updateDepth == 0, let view, let descriptor else { return }
70
+ let attached = view.window != nil && !view.isHidden && !view.bounds.isEmpty
71
+ let screenScale = max(1, view.window?.screen.scale ?? view.traitCollection.displayScale)
72
+ let fit = min(view.bounds.width / pointSize.width, view.bounds.height / pointSize.height)
73
+ let rasterScale = ceil(screenScale * max(1, fit))
74
+ let next = "\(descriptor.name)|\(pointSize)|\(rasterScale)|\(screenScale)|\(attached)|\(template)"
75
+ guard force || next != identity else { return }
76
+ identity = next; revision &+= 1
77
+ let generation = revision
78
+ OneKeyNamedSVG.shared.cancel(owner: owner)
79
+ currentImage = placeholder
80
+ apply(currentImage)
81
+ guard attached else { return }
82
+ if descriptor.name == "Circle" {
83
+ let image = UIGraphicsImageRenderer(size: pointSize).image { _ in
84
+ UIColor.black.setFill(); UIBezierPath(ovalIn: CGRect(origin: .zero, size: pointSize)).fill()
85
+ }
86
+ currentImage = image.withRenderingMode(.alwaysTemplate); apply(currentImage); return
87
+ }
88
+ let sourceSize = descriptor.size
89
+ let sourceScale = sizeOverride == nil ? rasterScale : ceil(rasterScale * max(pointSize.width / sourceSize.width, pointSize.height / sourceSize.height))
90
+ let pixelWidth = ceil(sourceSize.width * sourceScale), pixelHeight = ceil(sourceSize.height * sourceScale)
91
+ guard pixelWidth.isFinite, pixelHeight.isFinite, pixelWidth > 0, pixelHeight > 0,
92
+ pixelWidth <= 4096, pixelHeight <= 4096, pixelWidth * pixelHeight <= 4_194_304 else { return }
93
+ OneKeyNamedSVG.shared.request(owner: owner, name: descriptor.name, color: "rgba(0,0,0,1)",
94
+ width: Int(pixelWidth), height: Int(pixelHeight),
95
+ scale: screenScale) { [weak self] image in
96
+ guard let self, self.revision == generation, let view = self.view,
97
+ view.window != nil, !view.isHidden else { return }
98
+ guard let cgImage = image?.cgImage else { return }
99
+ var result = UIImage(cgImage: cgImage, scale: sourceScale, orientation: .up)
100
+ if let size = self.sizeOverride {
101
+ let format = UIGraphicsImageRendererFormat(); format.scale = rasterScale
102
+ result = UIGraphicsImageRenderer(size: size, format: format).image { _ in
103
+ result.draw(in: CGRect(origin: .zero, size: size))
104
+ }
105
+ }
106
+ self.currentImage = result.withRenderingMode(self.template ? .alwaysTemplate : .alwaysOriginal)
107
+ self.apply(self.currentImage)
108
+ }
109
+ }
110
+
111
+ deinit { OneKeyNamedSVG.shared.cancel(owner: owner) }
112
+ }
113
+
114
+ final class NativeListIconImageView: UIImageView {
115
+ private var applying = false
116
+ private lazy var iconBinding = NativeListIconBinding(view: self) { [weak self] image in
117
+ guard let self else { return }
118
+ self.applying = true; self.image = image; self.applying = false
119
+ }
120
+ func bindIcon(_ name: String?, size: CGSize? = nil) { iconBinding.bind(name, size: size) }
121
+ func beginIconUpdate() { iconBinding.beginUpdate() }
122
+ func endIconUpdate() { iconBinding.endUpdate() }
123
+ override var image: UIImage? { didSet { if !applying { iconBinding.clear(display: image == nil) } } }
124
+ override var isHidden: Bool { didSet { iconBinding.refresh() } }
125
+ override func layoutSubviews() { super.layoutSubviews(); iconBinding.refresh() }
126
+ override func didMoveToWindow() { super.didMoveToWindow(); iconBinding.refresh() }
127
+ override func tintColorDidChange() { super.tintColorDidChange(); iconBinding.retint() }
128
+ override func traitCollectionDidChange(_ previous: UITraitCollection?) {
129
+ super.traitCollectionDidChange(previous); iconBinding.refresh()
130
+ }
131
+ }
132
+
133
+ class NativeListIconButton: UIButton {
134
+ private var applying = false
135
+ private lazy var iconBinding = NativeListIconBinding(view: self) { [weak self] image in
136
+ guard let self else { return }
137
+ self.applying = true
138
+ self.setImage(image, for: .normal)
139
+ self.setImage(image?.withTintColor(self.tintColor, renderingMode: .alwaysOriginal), for: .disabled)
140
+ self.applying = false
141
+ }
142
+ func bindIcon(_ name: String?, size: CGSize? = nil) { iconBinding.bind(name, size: size) }
143
+ func beginIconUpdate() { iconBinding.beginUpdate() }
144
+ func endIconUpdate() { iconBinding.endUpdate() }
145
+ override func setImage(_ image: UIImage?, for state: UIControl.State) {
146
+ if !applying && state == .normal { iconBinding.clear(display: image == nil) }
147
+ super.setImage(image, for: state)
148
+ }
149
+ override var isHidden: Bool { didSet { iconBinding.refresh() } }
150
+ override func layoutSubviews() { super.layoutSubviews(); iconBinding.refresh() }
151
+ override func didMoveToWindow() { super.didMoveToWindow(); iconBinding.refresh() }
152
+ override func tintColorDidChange() { super.tintColorDidChange(); iconBinding.retint() }
153
+ override func traitCollectionDidChange(_ previous: UITraitCollection?) {
154
+ super.traitCollectionDidChange(previous); iconBinding.refresh()
155
+ }
156
+ }
157
+
158
+ /// A synchronous renderer bind may clear then restore the same glyph descriptor.
159
+ final class NativeListIconUpdate {
160
+ private var images: [NativeListIconImageView] = []
161
+ private var buttons: [NativeListIconButton] = []
162
+ init(_ root: UIView) {
163
+ func collect(_ view: UIView) {
164
+ if let image = view as? NativeListIconImageView { images.append(image); image.beginIconUpdate() }
165
+ if let button = view as? NativeListIconButton { buttons.append(button); button.beginIconUpdate() }
166
+ view.subviews.forEach(collect)
167
+ }
168
+ collect(root)
169
+ }
170
+ func finish() { images.forEach { $0.endIconUpdate() }; buttons.forEach { $0.endIconUpdate() } }
171
+ }