@onekeyfe/react-native-native-list 6.7.0-alpha.426 → 6.7.0-alpha.430
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/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +82 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +6 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListTitleSegmentLayout.kt +38 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTitleSegmentsView.kt +105 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListTitleSegmentLayoutTest.kt +65 -0
- package/docs/ROW_TEMPLATES.md +8 -1
- package/docs/SPEC.md +87 -0
- package/ios/NativeListIdentityRenderer.swift +89 -6
- package/ios/NativeListTitleSegmentLayout.swift +32 -0
- package/ios/NativeListTitleSegmentsLabel.swift +80 -0
- package/ios/tests/NativeListIdentityTitleIconLayoutTests.swift +32 -0
- package/lib/module/models.js +2 -0
- package/lib/module/validation.js +70 -4
- package/lib/module/web/NativeListWebEngine.js +1 -1
- package/lib/module/web/templates/IdentityRowRenderer.js +91 -0
- package/lib/typescript/src/models.d.ts +21 -1
- package/package.json +3 -3
- package/src/models.ts +23 -0
- package/src/validation.ts +120 -1
- package/src/web/NativeListWebEngine.ts +1 -1
- package/src/web/templates/IdentityRowRenderer.ts +138 -0
|
@@ -31,6 +31,8 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
31
31
|
private val titleLine = PackedTitleLineLayout(context)
|
|
32
32
|
private val title = NativeListTextView(context)
|
|
33
33
|
private val titleIcon = OneKeyIconView(context)
|
|
34
|
+
private val titleLeadingIcon = OneKeyIconView(context)
|
|
35
|
+
private val titleSegments = NativeListTitleSegmentsView(context)
|
|
34
36
|
private var titleImage: NativeListImageSlot? = null
|
|
35
37
|
private val subtitle = NativeListTextView(context)
|
|
36
38
|
private val tertiary = NativeListTextView(context)
|
|
@@ -109,8 +111,13 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
109
111
|
mainColumn.gravity = Gravity.CENTER_VERTICAL
|
|
110
112
|
titleLine.orientation = HORIZONTAL
|
|
111
113
|
titleLine.gravity = Gravity.CENTER_VERTICAL
|
|
114
|
+
titleLine.addView(titleLeadingIcon, LayoutParams(dp(16), dp(16)).apply { marginEnd = dp(6) })
|
|
112
115
|
titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
|
116
|
+
titleLine.addView(titleSegments, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
|
113
117
|
titleLine.addView(titleIcon, LayoutParams(dp(20), dp(20)).apply { marginStart = dp(4) })
|
|
118
|
+
titleLine.titleChild = title
|
|
119
|
+
titleLeadingIcon.visibility = GONE
|
|
120
|
+
titleSegments.visibility = GONE
|
|
114
121
|
titleLine.addView(badgeLine)
|
|
115
122
|
trailingColumn.onAction = { key, view, slot, target ->
|
|
116
123
|
emitAction(key, view, "trailingAccessory", slot, target, trailingColumn.anchorInset(view))
|
|
@@ -223,6 +230,12 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
223
230
|
titleIcon.visibility = GONE
|
|
224
231
|
titleIcon.contentDescription = null
|
|
225
232
|
titleIcon.setOnClickListener(null)
|
|
233
|
+
titleLeadingIcon.visibility = GONE
|
|
234
|
+
titleLeadingIcon.contentDescription = null
|
|
235
|
+
titleSegments.visibility = GONE
|
|
236
|
+
titleSegments.clearRuns()
|
|
237
|
+
titleSegments.layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
|
|
238
|
+
titleLine.titleChild = title
|
|
226
239
|
for (view in listOf(title, subtitle, tertiary, badgeLine)) {
|
|
227
240
|
view.text = ""
|
|
228
241
|
view.visibility = GONE
|
|
@@ -343,6 +356,46 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
343
356
|
}
|
|
344
357
|
// Legacy: overhanging selector icons/checkboxes must not be clipped by row padding.
|
|
345
358
|
if (trailingColumn.parent === this && trailingColumn.requestsUnclippedHost) clipToPadding = false
|
|
359
|
+
if (titleSegments.visibility == VISIBLE) bindTitleRuns(item, theme)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Runs take the title's final text appearance and color unless they set
|
|
363
|
+
// their own tone or weight.
|
|
364
|
+
private fun bindTitleRuns(item: NativeListItem, theme: JSONObject?) {
|
|
365
|
+
titleSegments.typeface = title.typeface
|
|
366
|
+
titleSegments.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, title.textSize)
|
|
367
|
+
titleSegments.includeFontPadding = title.includeFontPadding
|
|
368
|
+
titleSegments.letterSpacing = title.letterSpacing
|
|
369
|
+
titleSegments.opticalOffsetY = title.opticalOffsetY
|
|
370
|
+
titleSegments.maxLines = 1
|
|
371
|
+
titleSegments.ellipsize = TextUtils.TruncateAt.END
|
|
372
|
+
TextViewCompat.setLineHeight(titleSegments, title.lineHeight)
|
|
373
|
+
val baseColor = title.currentTextColor
|
|
374
|
+
titleSegments.setTextColor(baseColor)
|
|
375
|
+
val segments = item.json.optJSONArray("titleSegments") ?: return
|
|
376
|
+
val runs =
|
|
377
|
+
(0 until segments.length()).mapNotNull { index ->
|
|
378
|
+
val segment = segments.optJSONObject(index) ?: return@mapNotNull null
|
|
379
|
+
NativeListTitleSegmentsView.Run(
|
|
380
|
+
text = segment.optString("text"),
|
|
381
|
+
typeface =
|
|
382
|
+
when (segment.optString("fontWeight")) {
|
|
383
|
+
"medium" -> fonts.medium(context)
|
|
384
|
+
"regular" -> fonts.regular(context)
|
|
385
|
+
else -> null
|
|
386
|
+
},
|
|
387
|
+
color =
|
|
388
|
+
when (segment.optString("tone")) {
|
|
389
|
+
"secondary" -> color(theme, "secondaryText", "#0000009B")
|
|
390
|
+
"primary" -> color(theme, "primaryText", "#000000DF")
|
|
391
|
+
else -> baseColor
|
|
392
|
+
},
|
|
393
|
+
shrink = segment.optBoolean("shrink", false),
|
|
394
|
+
maxWidthRatio =
|
|
395
|
+
if (segment.has("maxWidthRatio")) segment.optDouble("maxWidthRatio").toFloat() else null,
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
titleSegments.setRuns(runs, item.json.optString("title"))
|
|
346
399
|
}
|
|
347
400
|
|
|
348
401
|
private fun styleGap(column: LinearLayout, gap: Int) {
|
|
@@ -476,7 +529,8 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
476
529
|
val imageWidth = titleImage?.view?.takeIf { it.visibility == VISIBLE }?.let {
|
|
477
530
|
it.layoutParams.width + dp(4)
|
|
478
531
|
} ?: 0
|
|
479
|
-
val icon = (if (titleIcon.visibility == VISIBLE) dp(24) else 0) + imageWidth
|
|
532
|
+
val icon = (if (titleIcon.visibility == VISIBLE) dp(24) else 0) + imageWidth +
|
|
533
|
+
(if (titleLeadingIcon.visibility == VISIBLE) dp(22) else 0)
|
|
480
534
|
(inner * ratio).roundToInt()
|
|
481
535
|
.minus(leading.width + leading.marginEnd + icon)
|
|
482
536
|
.coerceAtLeast(0)
|
|
@@ -487,9 +541,10 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
487
541
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
488
542
|
if (titleSuffixLabels.isNotEmpty()) {
|
|
489
543
|
val fixedWidth =
|
|
490
|
-
(
|
|
544
|
+
(0 until titleLine.childCount).sumOf { index ->
|
|
491
545
|
val view = titleLine.getChildAt(index)
|
|
492
|
-
if (view.visibility == GONE ||
|
|
546
|
+
if (view.visibility == GONE || view === titleLine.titleChild ||
|
|
547
|
+
titleSuffixLabels.any { it === view }) 0
|
|
493
548
|
else {
|
|
494
549
|
val margins = view.layoutParams as MarginLayoutParams
|
|
495
550
|
view.measuredWidth + margins.leftMargin + margins.rightMargin
|
|
@@ -753,6 +808,30 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
|
|
|
753
808
|
titleLine.packsChildrenAtStart = true
|
|
754
809
|
title.ellipsize = TextUtils.TruncateAt.END
|
|
755
810
|
showText(title, item.json.optString("title"), item.json.optInt("titleLines", 1))
|
|
811
|
+
// Runs replace the drawn title except where `title` itself is highlighted;
|
|
812
|
+
// their content is bound once the title style is final.
|
|
813
|
+
if ((item.json.optJSONArray("titleSegments")?.length() ?: 0) > 0 &&
|
|
814
|
+
(item.json.optJSONArray("titleMatch")?.length() ?: 0) == 0
|
|
815
|
+
) {
|
|
816
|
+
title.visibility = GONE
|
|
817
|
+
titleSegments.visibility = VISIBLE
|
|
818
|
+
titleLine.titleChild = titleSegments
|
|
819
|
+
}
|
|
820
|
+
item.json.optJSONObject("titleLeadingIcon")?.let { icon ->
|
|
821
|
+
titleLeadingIcon.iconName = icon.optString("name")
|
|
822
|
+
titleLeadingIcon.glyphSizeDp = 16
|
|
823
|
+
titleLeadingIcon.useSourceScale = sourceScale
|
|
824
|
+
titleLeadingIcon.tintColor = safeColor(
|
|
825
|
+
icon.optString("tintColor").takeIf(String::isNotEmpty),
|
|
826
|
+
color(theme, "iconSubdued", "#00000072"),
|
|
827
|
+
)
|
|
828
|
+
titleLeadingIcon.contentDescription =
|
|
829
|
+
icon.optString("accessibilityLabel").takeIf(String::isNotEmpty)
|
|
830
|
+
titleLeadingIcon.importantForAccessibility =
|
|
831
|
+
if (titleLeadingIcon.contentDescription == null) View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
832
|
+
else View.IMPORTANT_FOR_ACCESSIBILITY_YES
|
|
833
|
+
titleLeadingIcon.visibility = VISIBLE
|
|
834
|
+
}
|
|
756
835
|
item.json.optJSONArray("titleSuffixes")?.let { suffixes ->
|
|
757
836
|
for (index in 0 until suffixes.length()) {
|
|
758
837
|
val suffix = suffixes.optJSONObject(index) ?: continue
|
|
@@ -109,6 +109,9 @@ internal class PackedTitleLineLayout(context: android.content.Context) : LinearL
|
|
|
109
109
|
var packsChildrenAtStart = false
|
|
110
110
|
// OneKey patch: optional cap for the Market subtitle's localized name.
|
|
111
111
|
var leadingTextMaxWidth = Int.MAX_VALUE
|
|
112
|
+
// The text view that takes what the other children leave; the first child
|
|
113
|
+
// when unset, so a glyph may precede the title once this names it.
|
|
114
|
+
var titleChild: TextView? = null
|
|
112
115
|
|
|
113
116
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
114
117
|
if (!packsChildrenAtStart || childCount < 2) {
|
|
@@ -116,13 +119,13 @@ internal class PackedTitleLineLayout(context: android.content.Context) : LinearL
|
|
|
116
119
|
return
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
val title = getChildAt(0)
|
|
122
|
+
val title = titleChild?.takeIf { it.parent === this } ?: getChildAt(0)
|
|
120
123
|
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
|
121
124
|
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
|
122
125
|
var accessoryWidth = 0
|
|
123
|
-
for (index in
|
|
126
|
+
for (index in 0 until childCount) {
|
|
124
127
|
val child = getChildAt(index)
|
|
125
|
-
if (child.visibility == GONE) continue
|
|
128
|
+
if (child === title || child.visibility == GONE) continue
|
|
126
129
|
measureChildWithMargins(
|
|
127
130
|
child,
|
|
128
131
|
widthMeasureSpec,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Widths for identity title runs (docs/SPEC.md, "Identity title segments and
|
|
5
|
+
* leading title icon"). A run's base width is its natural width under its cap;
|
|
6
|
+
* the one shrinking run takes what the others leave; runs that do not shrink
|
|
7
|
+
* and still overflow are cut from the end of the line.
|
|
8
|
+
*/
|
|
9
|
+
internal fun nativeListTitleSegmentWidths(
|
|
10
|
+
natural: List<Float>,
|
|
11
|
+
shrink: List<Boolean>,
|
|
12
|
+
maxWidthRatio: List<Float?>,
|
|
13
|
+
available: Float,
|
|
14
|
+
): List<Float> {
|
|
15
|
+
val line = available.coerceAtLeast(0f)
|
|
16
|
+
val widths =
|
|
17
|
+
natural.indices
|
|
18
|
+
.map { index ->
|
|
19
|
+
val width = natural[index].coerceAtLeast(0f)
|
|
20
|
+
val ratio = maxWidthRatio.getOrNull(index)
|
|
21
|
+
if (ratio == null) width else minOf(width, ratio * line)
|
|
22
|
+
}
|
|
23
|
+
.toMutableList()
|
|
24
|
+
val shrinking = shrink.indexOf(true)
|
|
25
|
+
if (shrinking in widths.indices) {
|
|
26
|
+
val others = widths.indices.filter { it != shrinking }.sumOf { widths[it].toDouble() }.toFloat()
|
|
27
|
+
widths[shrinking] = minOf(widths[shrinking], line - others).coerceAtLeast(0f)
|
|
28
|
+
}
|
|
29
|
+
var overflow = widths.sum() - line
|
|
30
|
+
var index = widths.lastIndex
|
|
31
|
+
while (overflow > 0f && index >= 0) {
|
|
32
|
+
val cut = minOf(widths[index], overflow)
|
|
33
|
+
widths[index] -= cut
|
|
34
|
+
overflow -= cut
|
|
35
|
+
index -= 1
|
|
36
|
+
}
|
|
37
|
+
return widths
|
|
38
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
import android.graphics.Typeface
|
|
4
|
+
import android.text.SpannableStringBuilder
|
|
5
|
+
import android.text.Spanned
|
|
6
|
+
import android.text.TextPaint
|
|
7
|
+
import android.text.TextUtils
|
|
8
|
+
import android.text.style.ForegroundColorSpan
|
|
9
|
+
import android.text.style.MetricAffectingSpan
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Identity title runs on one line. The text is rebuilt for each measured width
|
|
13
|
+
* from the widths `nativeListTitleSegmentWidths` allocates, so a run gives up
|
|
14
|
+
* width without the others; accessibility reads the row's `title` instead.
|
|
15
|
+
*/
|
|
16
|
+
internal class NativeListTitleSegmentsView(context: android.content.Context) :
|
|
17
|
+
NativeListTextView(context) {
|
|
18
|
+
class Run(
|
|
19
|
+
val text: String,
|
|
20
|
+
// Null keeps the view's typeface, which follows the title's.
|
|
21
|
+
val typeface: Typeface?,
|
|
22
|
+
val color: Int,
|
|
23
|
+
val shrink: Boolean,
|
|
24
|
+
val maxWidthRatio: Float?,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
private var runs: List<Run> = emptyList()
|
|
28
|
+
private var builtForWidth = -1
|
|
29
|
+
|
|
30
|
+
fun setRuns(runs: List<Run>, spokenText: String) {
|
|
31
|
+
this.runs = runs
|
|
32
|
+
builtForWidth = -1
|
|
33
|
+
contentDescription = spokenText.takeIf(String::isNotEmpty)
|
|
34
|
+
text = runs.joinToString("") { it.text }
|
|
35
|
+
requestLayout()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fun clearRuns() {
|
|
39
|
+
runs = emptyList()
|
|
40
|
+
builtForWidth = -1
|
|
41
|
+
contentDescription = null
|
|
42
|
+
text = ""
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
46
|
+
if (runs.isEmpty()) {
|
|
47
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
var outer =
|
|
51
|
+
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) Int.MAX_VALUE
|
|
52
|
+
else MeasureSpec.getSize(widthMeasureSpec)
|
|
53
|
+
if (maxWidth in 0 until outer) outer = maxWidth
|
|
54
|
+
val available =
|
|
55
|
+
if (outer == Int.MAX_VALUE) outer
|
|
56
|
+
else (outer - compoundPaddingLeft - compoundPaddingRight).coerceAtLeast(0)
|
|
57
|
+
if (available != builtForWidth) {
|
|
58
|
+
builtForWidth = available
|
|
59
|
+
// The same width rebuilds the same text, so the relayout this asks for
|
|
60
|
+
// measures once more and stops.
|
|
61
|
+
setText(build(available), BufferType.SPANNABLE)
|
|
62
|
+
}
|
|
63
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
64
|
+
// The runs fill what the title line leaves, so a ratio is a share of it.
|
|
65
|
+
if (outer != Int.MAX_VALUE) setMeasuredDimension(outer, measuredHeight)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private fun build(available: Int): CharSequence {
|
|
69
|
+
val paints = runs.map { run -> TextPaint(paint).apply { run.typeface?.let { typeface = it } } }
|
|
70
|
+
val natural = runs.mapIndexed { index, run -> paints[index].measureText(run.text) }
|
|
71
|
+
val widths =
|
|
72
|
+
nativeListTitleSegmentWidths(
|
|
73
|
+
natural,
|
|
74
|
+
runs.map { it.shrink },
|
|
75
|
+
runs.map { it.maxWidthRatio },
|
|
76
|
+
if (available == Int.MAX_VALUE) Float.MAX_VALUE else available.toFloat(),
|
|
77
|
+
)
|
|
78
|
+
val built = SpannableStringBuilder()
|
|
79
|
+
runs.forEachIndexed { index, run ->
|
|
80
|
+
val width = widths[index]
|
|
81
|
+
if (width <= 0f) return@forEachIndexed
|
|
82
|
+
val shown =
|
|
83
|
+
if (width >= natural[index]) run.text
|
|
84
|
+
else TextUtils.ellipsize(run.text, paints[index], width, TextUtils.TruncateAt.END)
|
|
85
|
+
if (shown.isEmpty()) return@forEachIndexed
|
|
86
|
+
val start = built.length
|
|
87
|
+
built.append(shown)
|
|
88
|
+
built.setSpan(ForegroundColorSpan(run.color), start, built.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
89
|
+
run.typeface?.let {
|
|
90
|
+
built.setSpan(TypefaceRunSpan(it), start, built.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return built
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private class TypefaceRunSpan(private val typeface: Typeface) : MetricAffectingSpan() {
|
|
98
|
+
override fun updateMeasureState(paint: TextPaint) {
|
|
99
|
+
paint.typeface = typeface
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
override fun updateDrawState(paint: TextPaint) {
|
|
103
|
+
paint.typeface = typeface
|
|
104
|
+
}
|
|
105
|
+
}
|
package/android/src/test/java/com/margelo/nitro/nativelist/NativeListTitleSegmentLayoutTest.kt
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class NativeListTitleSegmentLayoutTest {
|
|
7
|
+
// Wallet / Account as recipient rows send it: the wallet shrinks, the
|
|
8
|
+
// account is capped at 70% of the line.
|
|
9
|
+
private fun walletFirst(wallet: Float, account: Float, line: Float) =
|
|
10
|
+
nativeListTitleSegmentWidths(
|
|
11
|
+
listOf(wallet, 20f, account),
|
|
12
|
+
listOf(true, false, false),
|
|
13
|
+
listOf(null, null, 0.7f),
|
|
14
|
+
line,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
private fun assertWidths(expected: List<Float>, actual: List<Float>) {
|
|
18
|
+
assertEquals(expected.size, actual.size)
|
|
19
|
+
expected.zip(actual).forEach { (want, got) -> assertEquals(want, got, 0.001f) }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test
|
|
23
|
+
fun keepsNaturalWidthsWhenTheyFit() = assertWidths(listOf(80f, 20f, 90f), walletFirst(80f, 90f, 300f))
|
|
24
|
+
|
|
25
|
+
@Test
|
|
26
|
+
fun theShrinkingRunGivesWayFirst() = assertWidths(listOf(130f, 20f, 100f), walletFirst(200f, 100f, 250f))
|
|
27
|
+
|
|
28
|
+
@Test
|
|
29
|
+
fun aCappedRunStopsAtItsShareOfTheLine() = assertWidths(listOf(70f, 20f, 210f), walletFirst(100f, 400f, 300f))
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun theAccountShrinksWhenTheWalletMustStayWhole() =
|
|
33
|
+
assertWidths(
|
|
34
|
+
listOf(210f, 20f, 70f),
|
|
35
|
+
nativeListTitleSegmentWidths(
|
|
36
|
+
listOf(300f, 20f, 150f),
|
|
37
|
+
listOf(false, false, true),
|
|
38
|
+
listOf(0.7f, null, null),
|
|
39
|
+
300f,
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@Test
|
|
44
|
+
fun runsThatDoNotShrinkAreCutFromTheEnd() {
|
|
45
|
+
assertWidths(
|
|
46
|
+
listOf(100f, 50f, 0f),
|
|
47
|
+
nativeListTitleSegmentWidths(
|
|
48
|
+
listOf(100f, 100f, 100f),
|
|
49
|
+
listOf(false, false, true),
|
|
50
|
+
listOf(null, null, null),
|
|
51
|
+
150f,
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
assertWidths(
|
|
55
|
+
listOf(100f, 20f),
|
|
56
|
+
nativeListTitleSegmentWidths(listOf(100f, 100f), listOf(false, false), listOf(null, null), 120f),
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Test
|
|
61
|
+
fun aNonPositiveLineLeavesNothing() {
|
|
62
|
+
assertWidths(listOf(0f, 0f, 0f), walletFirst(80f, 90f, 0f))
|
|
63
|
+
assertWidths(listOf(0f, 0f, 0f), walletFirst(80f, 90f, -10f))
|
|
64
|
+
}
|
|
65
|
+
}
|
package/docs/ROW_TEMPLATES.md
CHANGED
|
@@ -54,7 +54,14 @@ separately from source implementation in STYLE_SPEC §9.
|
|
|
54
54
|
|
|
55
55
|
- **Use:** an identifiable asset, account, wallet, network or setting.
|
|
56
56
|
- **Required data:** `leading`, `title`; optional `subtitle`, `tertiary`, `badges`,
|
|
57
|
-
`trailing`, `leadingAction`, `titleIcon`, Web-only `secondaryTitleIcon`,
|
|
57
|
+
`trailing`, `leadingAction`, `titleIcon`, Web-only `secondaryTitleIcon`, `titleSegments`,
|
|
58
|
+
`titleLeadingIcon`, match ranges and structured subtitle segments.
|
|
59
|
+
- **Title runs:** optional `titleSegments` draw 1...4 runs with their own tone and
|
|
60
|
+
weight in place of `title`, on one line. One run may shrink; the others keep
|
|
61
|
+
their width under an optional `maxWidthRatio` cap. `title` stays the spoken
|
|
62
|
+
text. See SPEC.md.
|
|
63
|
+
- **Leading title icon:** optional `titleLeadingIcon` draws a 16-point glyph 6
|
|
64
|
+
points before the title; it is decorative unless labelled.
|
|
58
65
|
- **Static title icon:** optional 20-point icon immediately after the title with
|
|
59
66
|
a 4-point gap. It has no click action; an optional Web `hoverActionKey`
|
|
60
67
|
emits an icon-anchored tooltip request on hover or touch. A trailing `button` is the fixed small
|
package/docs/SPEC.md
CHANGED
|
@@ -188,6 +188,93 @@ and Web acceptance must cover narrow addresses, dynamic type/fonts, long press
|
|
|
188
188
|
versus tap, keyboard interaction, and the horizontal warning border. Source or
|
|
189
189
|
build success alone is not runtime evidence.
|
|
190
190
|
|
|
191
|
+
### Identity title segments and leading title icon
|
|
192
|
+
|
|
193
|
+
Status: implemented in source for iOS, Android and Web; device and Web runtime
|
|
194
|
+
acceptance is pending. These fields travel in snapshot/patch JSON and do not
|
|
195
|
+
change the Nitro ABI. They serve recipient rows whose title is a wallet and an
|
|
196
|
+
account name in two styles, and address book rows marked by an icon before the
|
|
197
|
+
name.
|
|
198
|
+
|
|
199
|
+
`identity.titleSegments` is an optional list of 1...4 runs drawn on the title
|
|
200
|
+
line in place of `title`:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
titleSegments?: readonly {
|
|
204
|
+
text: string;
|
|
205
|
+
tone?: 'primary' | 'secondary';
|
|
206
|
+
fontWeight?: 'regular' | 'medium';
|
|
207
|
+
shrink?: boolean;
|
|
208
|
+
maxWidthRatio?: number; // (0, 1]
|
|
209
|
+
}[];
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- `title` stays required. It is the spoken label and the fallback text, so it
|
|
213
|
+
carries the same words as the runs; every renderer exposes `title`, not the
|
|
214
|
+
runs, to accessibility.
|
|
215
|
+
- Each run uses the title's font family, size and line height, including
|
|
216
|
+
`style.title` and the configured fonts. `tone` picks the primary or secondary
|
|
217
|
+
text color and defaults to the title color; `fontWeight` picks the weight and
|
|
218
|
+
defaults to the title weight.
|
|
219
|
+
- The runs share one line. `titleLines` and `style.title.lines` do not apply.
|
|
220
|
+
- Layout. The runs take the whole width the title line leaves after the
|
|
221
|
+
leading title icon and the title accessories (title icon, title image,
|
|
222
|
+
suffixes, badges), so those accessories sit at the line's end rather than
|
|
223
|
+
right after the text. W is that width:
|
|
224
|
+
1. A run's base width is its natural width, capped at `maxWidthRatio x W`
|
|
225
|
+
when given. A run longer than its cap truncates at the cap with a tail
|
|
226
|
+
ellipsis.
|
|
227
|
+
2. At most one run sets `shrink: true`. Every other run keeps its base width.
|
|
228
|
+
3. The shrinking run gets `min(base, W - sum of the other runs)` and
|
|
229
|
+
truncates with a tail ellipsis; it may shrink to zero width.
|
|
230
|
+
4. When the runs that do not shrink exceed W on their own, the line is cut at
|
|
231
|
+
its end. Native renderers end the last visible run with an ellipsis; Web
|
|
232
|
+
clips it. This divergence is accepted: callers avoid the case with
|
|
233
|
+
`maxWidthRatio`.
|
|
234
|
+
- Development validation rejects an empty or oversized list, blank or
|
|
235
|
+
oversized text (the 4096-code-unit text bound), unknown keys, tones or
|
|
236
|
+
weights, a ratio outside `(0, 1]`, more than one shrinking run, `titleMatch`
|
|
237
|
+
on the same row (its ranges index `title`), and
|
|
238
|
+
`presentation: 'walletSidebar'`. Production treats the runs as decoration:
|
|
239
|
+
an invalid list is dropped and the row shows `title`. Renderers draw `title`
|
|
240
|
+
instead of runs on a row that also has `titleMatch`, and ignore runs and the
|
|
241
|
+
leading icon with `presentation: 'walletSidebar'`.
|
|
242
|
+
|
|
243
|
+
`identity.titleLeadingIcon` is an optional `{ name, tintColor?,
|
|
244
|
+
accessibilityLabel? }`: a 16-point glyph before the title text or its runs,
|
|
245
|
+
with a 6-point gap, vertically centered on the title line. It keeps its 16
|
|
246
|
+
points: a long title, or its shrinking run, truncates first. The name comes from
|
|
247
|
+
the same closed icon set as `titleIcon`; an unmapped name leaves an empty
|
|
248
|
+
16-point slot. `tintColor` is a hex `#RRGGBB` or `#RRGGBBAA` color and defaults
|
|
249
|
+
to the theme's `iconSubdued`. Without `accessibilityLabel` the glyph is hidden
|
|
250
|
+
from accessibility. It has no action of its own. Development validation
|
|
251
|
+
rejects unknown keys, a blank name, a malformed color and
|
|
252
|
+
`presentation: 'walletSidebar'`; production drops an invalid icon.
|
|
253
|
+
|
|
254
|
+
Both fields are identity patch fields. Omission in a full snapshot removes
|
|
255
|
+
them; omission in a patch keeps the current value. Rebind and recycle remove the
|
|
256
|
+
previous runs and glyph, so a row without them never shows another row's.
|
|
257
|
+
|
|
258
|
+
| Platform | Title runs | Leading icon |
|
|
259
|
+
| --- | --- | --- |
|
|
260
|
+
| iOS | `NativeListTitleSegmentsLabel` draws the runs at the widths that `NativeListTitleSegmentLayout.swift` allocates for its bounds; it fills the title row in place of the row's filler. | `NativeListIconImageView`, first in the title row stack. A plain title beside it drops below required horizontal compression resistance, as beside `titleIcon`, so the title truncates before the glyph narrows. |
|
|
261
|
+
| Android | `NativeListTitleSegmentsView`, a `TextView` whose text is rebuilt for each measured width from `NativeListTitleSegmentLayout.kt` and which measures to the width it is offered. `PackedTitleLineLayout.titleChild` names the flexible title view, so a view may precede it. | `OneKeyIconView`, first in the title line. |
|
|
262
|
+
| Web | A flex line that fills the title line: the shrinking run is `flex: 0 1 auto; min-width: 0`, the others `flex: 0 0 auto` with a `max-width` percentage for a cap. | An icon element before the title text. |
|
|
263
|
+
|
|
264
|
+
Conformance: `src/validation.ts` (`assertTitleSegments`,
|
|
265
|
+
`assertTitleLeadingIcon`, production sanitizing) with `validation.test.ts`;
|
|
266
|
+
`src/web/templates/IdentityRowRenderer.ts` with `IdentityTitleRunsWeb.test.ts`
|
|
267
|
+
(run styles, flex behavior, accessibility, rebind);
|
|
268
|
+
`ios/NativeListTitleSegmentLayout.swift` with the SwiftPM
|
|
269
|
+
`NativeListTitleSegmentLayoutTests`; the iOS leading glyph beside a long plain
|
|
270
|
+
title with the XCTest `NativeListIdentityTitleIconLayoutTests`;
|
|
271
|
+
`NativeListTitleSegmentLayout.kt` with the
|
|
272
|
+
JVM `NativeListTitleSegmentLayoutTest`. iOS, Android and Web runtime acceptance is pending: recipient rows
|
|
273
|
+
with long wallet and account names in both truncation directions, narrow rows,
|
|
274
|
+
dark mode, dynamic type and configured fonts, and recycling between rows with
|
|
275
|
+
and without runs or a leading icon. Source and build success alone are not
|
|
276
|
+
runtime evidence.
|
|
277
|
+
|
|
191
278
|
### DeFi position row controls
|
|
192
279
|
|
|
193
280
|
Status: implemented in local source, unreleased. Focused validation and Web
|
|
@@ -81,7 +81,8 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
81
81
|
let title = NativeListResolvedText(
|
|
82
82
|
item.data.string("title"), style: style.dictionary("title"), size: 16,
|
|
83
83
|
weight: .medium, color: .black, lineHeight: 24,
|
|
84
|
-
lines: item.data.
|
|
84
|
+
lines: item.data.dictionaries("titleSegments").isEmpty
|
|
85
|
+
? item.data.int("titleLines", default: 1) : 1, fonts: fonts)
|
|
85
86
|
let subtitle = NativeListResolvedText(
|
|
86
87
|
item.data.string("subtitle"), style: style.dictionary("subtitle"), size: 14,
|
|
87
88
|
color: .black, lineHeight: 20,
|
|
@@ -110,6 +111,12 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
110
111
|
private let titleRowStack = UIStackView()
|
|
111
112
|
private let titleLabel = NativeListTextLabel()
|
|
112
113
|
private let titleIconView = NativeListIconImageView()
|
|
114
|
+
private let titleLeadingIconView = NativeListIconImageView()
|
|
115
|
+
private let titleSegmentsLabel = NativeListTitleSegmentsLabel()
|
|
116
|
+
// Runs, when shown, take the title's place in the title row.
|
|
117
|
+
private var visibleTitleView: UIView {
|
|
118
|
+
titleSegmentsLabel.isHidden ? titleLabel : titleSegmentsLabel
|
|
119
|
+
}
|
|
113
120
|
private var titleImageSlot: NativeListImageSlot?
|
|
114
121
|
private var titleImageConstraints: [NSLayoutConstraint] = []
|
|
115
122
|
// Absorbs the title row's slack so the title keeps its intrinsic width and the
|
|
@@ -170,7 +177,9 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
170
177
|
mainStack.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
171
178
|
titleRowStack.axis = .horizontal
|
|
172
179
|
titleRowStack.alignment = .center
|
|
180
|
+
titleRowStack.addArrangedSubview(titleLeadingIconView)
|
|
173
181
|
titleRowStack.addArrangedSubview(titleLabel)
|
|
182
|
+
titleRowStack.addArrangedSubview(titleSegmentsLabel)
|
|
174
183
|
titleRowStack.addArrangedSubview(titleIconView)
|
|
175
184
|
titleRowStack.addArrangedSubview(badgeLabel)
|
|
176
185
|
titleRowStack.addArrangedSubview(titleRowFiller)
|
|
@@ -185,6 +194,18 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
185
194
|
])
|
|
186
195
|
titleIconView.contentMode = .scaleAspectFit
|
|
187
196
|
titleIconView.setContentHuggingPriority(.required, for: .horizontal)
|
|
197
|
+
titleLeadingIconView.translatesAutoresizingMaskIntoConstraints = false
|
|
198
|
+
let leadingIconWidth = titleLeadingIconView.widthAnchor.constraint(equalToConstant: 16)
|
|
199
|
+
leadingIconWidth.priority = UILayoutPriority(999)
|
|
200
|
+
NSLayoutConstraint.activate([
|
|
201
|
+
leadingIconWidth,
|
|
202
|
+
titleLeadingIconView.heightAnchor.constraint(equalToConstant: 16),
|
|
203
|
+
])
|
|
204
|
+
titleLeadingIconView.contentMode = .scaleAspectFit
|
|
205
|
+
titleLeadingIconView.setContentHuggingPriority(.required, for: .horizontal)
|
|
206
|
+
titleLeadingIconView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
207
|
+
titleLeadingIconView.isHidden = true
|
|
208
|
+
titleSegmentsLabel.isHidden = true
|
|
188
209
|
[titleRowStack, subtitleLabel, tertiaryLabel].forEach(mainStack.addArrangedSubview)
|
|
189
210
|
visual.translatesAutoresizingMaskIntoConstraints = false
|
|
190
211
|
leadingWidth = visual.widthAnchor.constraint(equalToConstant: 40)
|
|
@@ -249,6 +270,15 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
249
270
|
titleIconView.isHidden = true
|
|
250
271
|
titleIconView.accessibilityLabel = nil
|
|
251
272
|
titleIconView.isAccessibilityElement = false
|
|
273
|
+
titleLeadingIconView.image = nil
|
|
274
|
+
titleLeadingIconView.isHidden = true
|
|
275
|
+
titleLeadingIconView.accessibilityLabel = nil
|
|
276
|
+
titleLeadingIconView.isAccessibilityElement = false
|
|
277
|
+
titleSegmentsLabel.isHidden = true
|
|
278
|
+
titleSegmentsLabel.reset()
|
|
279
|
+
titleRowFiller.isHidden = false
|
|
280
|
+
titleSegmentsLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
281
|
+
titleSegmentsLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
252
282
|
titleImageSlot?.view.isHidden = true
|
|
253
283
|
titleImageSlot?.view.accessibilityLabel = nil
|
|
254
284
|
titleImageSlot?.view.isAccessibilityElement = false
|
|
@@ -310,7 +340,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
310
340
|
let label = titleIcon.string("accessibilityLabel")
|
|
311
341
|
titleIconView.accessibilityLabel = label.isEmpty ? nil : label
|
|
312
342
|
titleIconView.isAccessibilityElement = !label.isEmpty
|
|
313
|
-
titleRowStack.setCustomSpacing(4, after:
|
|
343
|
+
titleRowStack.setCustomSpacing(4, after: visibleTitleView)
|
|
314
344
|
// A long title truncates ahead of the 20pt glyph (Android caps the title
|
|
315
345
|
// at row width minus accessories, Web shrinks the title first); with the
|
|
316
346
|
// title at `.required` the icon's 999 width constraint collapsed to 0.
|
|
@@ -342,7 +372,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
342
372
|
]
|
|
343
373
|
titleImageConstraints.forEach { $0.priority = UILayoutPriority(999) }
|
|
344
374
|
NSLayoutConstraint.activate(titleImageConstraints)
|
|
345
|
-
titleRowStack.setCustomSpacing(4, after: titleIconView.isHidden ?
|
|
375
|
+
titleRowStack.setCustomSpacing(4, after: titleIconView.isHidden ? visibleTitleView : titleIconView)
|
|
346
376
|
titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
347
377
|
slot.bind(image, key: "\(item.key):titleImage", placeholder: "#00000000")
|
|
348
378
|
} else {
|
|
@@ -385,7 +415,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
385
415
|
titleRowStack.setCustomSpacing(
|
|
386
416
|
CGFloat(style.double("titleBadgeGap")),
|
|
387
417
|
after: titleImageSlot?.view.isHidden == false ? titleImageSlot!.view
|
|
388
|
-
: titleIconView.isHidden ?
|
|
418
|
+
: titleIconView.isHidden ? visibleTitleView : titleIconView)
|
|
389
419
|
}
|
|
390
420
|
}
|
|
391
421
|
if style["trailingGap"] != nil { trailingStack.spacing = CGFloat(style.double("trailingGap")) }
|
|
@@ -399,6 +429,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
399
429
|
labels.forEach { NativeListTextStyles.applyStyledText($0, text, fonts: fonts) }
|
|
400
430
|
}
|
|
401
431
|
}
|
|
432
|
+
if !titleSegmentsLabel.isHidden { bindTitleRuns(item, theme: theme, style: style) }
|
|
402
433
|
contentInsets = UIEdgeInsets(
|
|
403
434
|
top: topInset, left: leftInset, bottom: -bottomInset, right: -rightInset)
|
|
404
435
|
if root.axis == .horizontal {
|
|
@@ -464,6 +495,8 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
464
495
|
}
|
|
465
496
|
override func recycleContent() {
|
|
466
497
|
visual.recycle()
|
|
498
|
+
titleSegmentsLabel.reset()
|
|
499
|
+
titleLeadingIconView.image = nil
|
|
467
500
|
titleImageSlot?.recycle()
|
|
468
501
|
titleImageSlot?.view.isHidden = true
|
|
469
502
|
titleImageSlot?.view.accessibilityLabel = nil
|
|
@@ -512,6 +545,30 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
512
545
|
visual.bind(
|
|
513
546
|
descriptor, style: image, key: key, theme: currentTheme, isUnread: false, fonts: fonts)
|
|
514
547
|
}
|
|
548
|
+
// Runs take the title's font, size and color unless they set their own tone
|
|
549
|
+
// or weight; the title label holds the final style by now.
|
|
550
|
+
private func bindTitleRuns(_ item: NativeListItem, theme: [String: Any]?, style: [String: Any]) {
|
|
551
|
+
let baseFont = titleLabel.font ?? nativeListFont(ofSize: 16, weight: .medium, fonts: fonts)
|
|
552
|
+
let baseColor = titleLabel.textColor ?? nativeListColor(theme, "primaryText", "#202020")
|
|
553
|
+
let runs = item.data.dictionaries("titleSegments").map { segment in
|
|
554
|
+
let weight = segment.string("fontWeight")
|
|
555
|
+
let tone = segment.string("tone")
|
|
556
|
+
return NativeListTitleSegmentsLabel.Run(
|
|
557
|
+
text: segment.string("text"),
|
|
558
|
+
font: weight.isEmpty
|
|
559
|
+
? baseFont
|
|
560
|
+
: nativeListFont(
|
|
561
|
+
ofSize: baseFont.pointSize, weight: weight == "medium" ? .medium : .regular, fonts: fonts),
|
|
562
|
+
color: tone == "secondary"
|
|
563
|
+
? nativeListColor(theme, "secondaryText", "#646464")
|
|
564
|
+
: tone == "primary" ? nativeListColor(theme, "primaryText", "#202020") : baseColor,
|
|
565
|
+
shrink: segment.bool("shrink"),
|
|
566
|
+
maxWidthRatio: (segment["maxWidthRatio"] as? NSNumber).map { CGFloat(truncating: $0) })
|
|
567
|
+
}
|
|
568
|
+
titleSegmentsLabel.lineHeight = CGFloat(
|
|
569
|
+
style.dictionary("title")?.double("lineHeight", default: 24) ?? 24)
|
|
570
|
+
titleSegmentsLabel.configure(runs, spokenText: item.data.string("title"))
|
|
571
|
+
}
|
|
515
572
|
private func bindIdentity(
|
|
516
573
|
_ item: NativeListItem,
|
|
517
574
|
theme: [String: Any]?,
|
|
@@ -643,10 +700,36 @@ final class NativeListIdentityCell: NativeListRendererCell {
|
|
|
643
700
|
}
|
|
644
701
|
root.addArrangedSubview(mainStack)
|
|
645
702
|
show(titleLabel, item.data.string("title"), lines: item.data.int("titleLines", default: 1))
|
|
703
|
+
// Runs replace the drawn title except where `title` itself is highlighted;
|
|
704
|
+
// their content is bound once the title style is final.
|
|
705
|
+
if !item.data.dictionaries("titleSegments").isEmpty,
|
|
706
|
+
item.data.dictionaries("titleMatch").isEmpty
|
|
707
|
+
{
|
|
708
|
+
titleLabel.isHidden = true
|
|
709
|
+
titleSegmentsLabel.isHidden = false
|
|
710
|
+
// The runs fill what the title row leaves, so a ratio is a share of it.
|
|
711
|
+
titleRowFiller.isHidden = true
|
|
712
|
+
titleSegmentsLabel.setContentHuggingPriority(UILayoutPriority(1), for: .horizontal)
|
|
713
|
+
}
|
|
714
|
+
if let icon = item.data.dictionary("titleLeadingIcon") {
|
|
715
|
+
titleLeadingIconView.bindIcon(icon.string("name"))
|
|
716
|
+
titleLeadingIconView.tintColor = UIColor(
|
|
717
|
+
nativeListHex: icon.string("tintColor",
|
|
718
|
+
default: theme?["iconSubdued"] as? String ?? "#8D8D8D"),
|
|
719
|
+
fallback: .gray)
|
|
720
|
+
titleLeadingIconView.isHidden = false
|
|
721
|
+
let label = icon.string("accessibilityLabel")
|
|
722
|
+
titleLeadingIconView.accessibilityLabel = label.isEmpty ? nil : label
|
|
723
|
+
titleLeadingIconView.isAccessibilityElement = !label.isEmpty
|
|
724
|
+
titleRowStack.setCustomSpacing(6, after: titleLeadingIconView)
|
|
725
|
+
// As beside `titleIcon`: a plain title left at `.required` collapses the
|
|
726
|
+
// glyph's 999 width constraint instead of truncating.
|
|
727
|
+
titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
728
|
+
}
|
|
646
729
|
let titleSuffixes = item.data.dictionaries("titleSuffixes")
|
|
647
730
|
if !titleSuffixes.isEmpty {
|
|
648
|
-
|
|
649
|
-
let minimumTitleWidth =
|
|
731
|
+
visibleTitleView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
732
|
+
let minimumTitleWidth = visibleTitleView.widthAnchor.constraint(greaterThanOrEqualToConstant: 80)
|
|
650
733
|
minimumTitleWidth.priority = UILayoutPriority(751)
|
|
651
734
|
minimumTitleWidth.isActive = true
|
|
652
735
|
selectorConstraints.append(minimumTitleWidth)
|