@onekeyfe/react-native-native-list 6.7.0-alpha.430 → 6.7.0-alpha.435
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 +3 -82
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +3 -6
- package/docs/ROW_TEMPLATES.md +1 -8
- package/docs/SPEC.md +20 -87
- package/ios/NativeListIdentityRenderer.swift +13 -89
- package/ios/tests/NativeListAccountSelectorLayoutTests.swift +96 -0
- package/ios/tests/NativeListIdentityTitleIconLayoutTests.swift +0 -32
- package/lib/module/models.js +0 -2
- package/lib/module/validation.js +4 -70
- package/lib/module/web/NativeListWebEngine.js +1 -1
- package/lib/module/web/templates/IdentityRowRenderer.js +0 -91
- package/lib/typescript/src/NativeList.types.d.ts +2 -2
- package/lib/typescript/src/models.d.ts +1 -21
- package/package.json +9 -9
- package/src/NativeList.types.ts +2 -2
- package/src/models.ts +0 -23
- package/src/validation.ts +1 -120
- package/src/web/NativeListWebEngine.ts +1 -1
- package/src/web/templates/IdentityRowRenderer.ts +0 -138
- package/android/src/main/java/com/onekey/nativelist/NativeListTitleSegmentLayout.kt +0 -38
- package/android/src/main/java/com/onekey/nativelist/NativeListTitleSegmentsView.kt +0 -105
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListTitleSegmentLayoutTest.kt +0 -65
- package/ios/NativeListTitleSegmentLayout.swift +0 -32
- package/ios/NativeListTitleSegmentsLabel.swift +0 -80
|
@@ -30,79 +30,6 @@ function toneColor(
|
|
|
30
30
|
if (tone === 'secondary') return 'var(--nl-secondary)';
|
|
31
31
|
return fallback === 'secondary' ? 'var(--nl-secondary)' : 'var(--nl-primary)';
|
|
32
32
|
}
|
|
33
|
-
// Hidden from sight, still read: the runs themselves are hidden from
|
|
34
|
-
// assistive technology in favor of `title`.
|
|
35
|
-
const VISUALLY_HIDDEN: Partial<CSSStyleDeclaration> = {
|
|
36
|
-
position: 'absolute',
|
|
37
|
-
width: '1px',
|
|
38
|
-
height: '1px',
|
|
39
|
-
overflow: 'hidden',
|
|
40
|
-
clip: 'rect(0 0 0 0)',
|
|
41
|
-
whiteSpace: 'nowrap',
|
|
42
|
-
};
|
|
43
|
-
function appendTitleRuns(
|
|
44
|
-
document: Document,
|
|
45
|
-
target: HTMLElement,
|
|
46
|
-
row: IdentityRow
|
|
47
|
-
): void {
|
|
48
|
-
const runs = createElement(
|
|
49
|
-
document,
|
|
50
|
-
'span',
|
|
51
|
-
'ok-native-list-identity-title-runs'
|
|
52
|
-
);
|
|
53
|
-
runs.setAttribute('aria-hidden', 'true');
|
|
54
|
-
Object.assign(runs.style, {
|
|
55
|
-
display: 'flex',
|
|
56
|
-
flex: '1 1 auto',
|
|
57
|
-
minWidth: '0',
|
|
58
|
-
maxWidth: '100%',
|
|
59
|
-
overflow: 'hidden',
|
|
60
|
-
});
|
|
61
|
-
row.titleSegments?.forEach((segment) => {
|
|
62
|
-
const run = createElement(
|
|
63
|
-
document,
|
|
64
|
-
'span',
|
|
65
|
-
'ok-native-list-identity-title-run',
|
|
66
|
-
segment.text
|
|
67
|
-
);
|
|
68
|
-
// Only the shrinking run gives up width; a cap is a share of the line.
|
|
69
|
-
Object.assign(run.style, {
|
|
70
|
-
flex: segment.shrink ? '0 1 auto' : '0 0 auto',
|
|
71
|
-
minWidth: segment.shrink ? '0' : '',
|
|
72
|
-
maxWidth:
|
|
73
|
-
segment.maxWidthRatio === undefined
|
|
74
|
-
? ''
|
|
75
|
-
: `${segment.maxWidthRatio * 100}%`,
|
|
76
|
-
overflow: 'hidden',
|
|
77
|
-
textOverflow: 'ellipsis',
|
|
78
|
-
whiteSpace: 'pre',
|
|
79
|
-
});
|
|
80
|
-
if (segment.tone)
|
|
81
|
-
run.style.color =
|
|
82
|
-
segment.tone === 'secondary'
|
|
83
|
-
? 'var(--nl-secondary)'
|
|
84
|
-
: 'var(--nl-primary)';
|
|
85
|
-
if (segment.fontWeight === 'medium') {
|
|
86
|
-
run.style.fontWeight = '500';
|
|
87
|
-
run.style.fontFamily = 'var(--nl-font-medium)';
|
|
88
|
-
} else if (segment.fontWeight === 'regular') {
|
|
89
|
-
run.style.fontWeight = '400';
|
|
90
|
-
run.style.fontFamily = 'var(--nl-font-regular)';
|
|
91
|
-
}
|
|
92
|
-
setData(run, 'tone', segment.tone);
|
|
93
|
-
runs.appendChild(run);
|
|
94
|
-
});
|
|
95
|
-
const spoken = createElement(
|
|
96
|
-
document,
|
|
97
|
-
'span',
|
|
98
|
-
'ok-native-list-identity-title-spoken',
|
|
99
|
-
row.title
|
|
100
|
-
);
|
|
101
|
-
Object.assign(spoken.style, VISUALLY_HIDDEN);
|
|
102
|
-
target.replaceChildren(runs, spoken);
|
|
103
|
-
target.style.display = 'flex';
|
|
104
|
-
target.style.minWidth = '0';
|
|
105
|
-
}
|
|
106
33
|
function appendAccessories(
|
|
107
34
|
parent: HTMLElement,
|
|
108
35
|
document: Document,
|
|
@@ -268,18 +195,6 @@ function createIdentityRow(
|
|
|
268
195
|
fragment.appendChild(document.createTextNode(row.title.slice(offset)));
|
|
269
196
|
textTarget.prepend(fragment);
|
|
270
197
|
}
|
|
271
|
-
// The runs replace the plain title, except where `title` itself is
|
|
272
|
-
// highlighted or shown in the wallet sidebar.
|
|
273
|
-
const hasTitleRuns =
|
|
274
|
-
!!row.titleSegments?.length &&
|
|
275
|
-
presentation !== 'walletSidebar' &&
|
|
276
|
-
!row.titleMatch?.length;
|
|
277
|
-
if (hasTitleRuns) {
|
|
278
|
-
const textTarget =
|
|
279
|
-
titleElement.querySelector<HTMLElement>('[data-nl-slot="title"]') ??
|
|
280
|
-
titleElement;
|
|
281
|
-
appendTitleRuns(document, textTarget, row);
|
|
282
|
-
}
|
|
283
198
|
if (row.subtitleSegments?.length) {
|
|
284
199
|
column.querySelector('.ok-native-list-secondary')?.remove();
|
|
285
200
|
const segments = createElement(
|
|
@@ -476,58 +391,6 @@ function createIdentityRow(
|
|
|
476
391
|
badges.style.minWidth = '0';
|
|
477
392
|
}
|
|
478
393
|
}
|
|
479
|
-
if (hasTitleRuns) {
|
|
480
|
-
// The runs fill what the title line leaves, so a ratio is a share of
|
|
481
|
-
// that width and title accessories sit at the line's end.
|
|
482
|
-
const runs = titleElement.querySelector<HTMLElement>(
|
|
483
|
-
'.ok-native-list-identity-title-runs'
|
|
484
|
-
);
|
|
485
|
-
for (
|
|
486
|
-
let node = runs?.parentElement;
|
|
487
|
-
node && node !== titleElement;
|
|
488
|
-
node = node.parentElement
|
|
489
|
-
) {
|
|
490
|
-
node.style.display = 'flex';
|
|
491
|
-
node.style.flex = '1 1 auto';
|
|
492
|
-
node.style.minWidth = '0';
|
|
493
|
-
}
|
|
494
|
-
titleElement.style.display = 'flex';
|
|
495
|
-
titleElement.style.alignItems = 'center';
|
|
496
|
-
}
|
|
497
|
-
if (row.titleLeadingIcon && presentation !== 'walletSidebar') {
|
|
498
|
-
const descriptor = row.titleLeadingIcon;
|
|
499
|
-
if (titleElement.hasAttribute('data-nl-slot')) {
|
|
500
|
-
// Keep the glyph out of the title slot that text styles apply to.
|
|
501
|
-
const titleText = tagSlot(
|
|
502
|
-
createElement(document, 'span', 'ok-native-list-identity-title-text'),
|
|
503
|
-
'title'
|
|
504
|
-
);
|
|
505
|
-
while (titleElement.firstChild)
|
|
506
|
-
titleText.appendChild(titleElement.firstChild);
|
|
507
|
-
if (hasTitleRuns) {
|
|
508
|
-
titleText.style.display = 'flex';
|
|
509
|
-
titleText.style.minWidth = '0';
|
|
510
|
-
}
|
|
511
|
-
titleElement.removeAttribute('data-nl-slot');
|
|
512
|
-
titleElement.appendChild(titleText);
|
|
513
|
-
}
|
|
514
|
-
const icon = primitives.iconAction(
|
|
515
|
-
descriptor.name,
|
|
516
|
-
undefined,
|
|
517
|
-
undefined,
|
|
518
|
-
descriptor.tintColor
|
|
519
|
-
);
|
|
520
|
-
icon.className = 'ok-native-list-identity-title-leading-icon';
|
|
521
|
-
if (descriptor.accessibilityLabel)
|
|
522
|
-
icon.setAttribute('aria-label', descriptor.accessibilityLabel);
|
|
523
|
-
else icon.setAttribute('aria-hidden', 'true');
|
|
524
|
-
titleElement.style.display = 'flex';
|
|
525
|
-
titleElement.style.alignItems = 'center';
|
|
526
|
-
// 6px to the title, whatever gap the title line already has.
|
|
527
|
-
const lineGap = Number.parseFloat(titleElement.style.gap || '0') || 0;
|
|
528
|
-
icon.style.marginInlineEnd = `${Math.max(0, 6 - lineGap)}px`;
|
|
529
|
-
titleElement.prepend(icon);
|
|
530
|
-
}
|
|
531
394
|
if (presentation === 'walletSidebar' && row.badges?.length) {
|
|
532
395
|
const badges = createElement(
|
|
533
396
|
document,
|
|
@@ -540,7 +403,6 @@ function createIdentityRow(
|
|
|
540
403
|
column.appendChild(badges);
|
|
541
404
|
}
|
|
542
405
|
for (const key of ['title', 'subtitle'] as const) {
|
|
543
|
-
if (key === 'title' && hasTitleRuns) continue;
|
|
544
406
|
const lines = row.style?.[key]?.lines ?? row[`${key}Lines`];
|
|
545
407
|
if (lines !== undefined)
|
|
546
408
|
column
|
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
/// Widths for identity title runs (docs/SPEC.md, "Identity title segments
|
|
4
|
-
/// and leading title icon"). A run's base width is its natural width under
|
|
5
|
-
/// its cap; the one shrinking run takes what the others leave; runs that do
|
|
6
|
-
/// not shrink and still overflow are cut from the end of the line.
|
|
7
|
-
func nativeListTitleSegmentWidths(
|
|
8
|
-
natural: [CGFloat],
|
|
9
|
-
shrink: [Bool],
|
|
10
|
-
maxWidthRatio: [CGFloat?],
|
|
11
|
-
available: CGFloat
|
|
12
|
-
) -> [CGFloat] {
|
|
13
|
-
let line = max(0, available)
|
|
14
|
-
var widths = natural.indices.map { index -> CGFloat in
|
|
15
|
-
let width = max(0, natural[index])
|
|
16
|
-
guard index < maxWidthRatio.count, let ratio = maxWidthRatio[index] else { return width }
|
|
17
|
-
return min(width, ratio * line)
|
|
18
|
-
}
|
|
19
|
-
if let shrinking = shrink.firstIndex(of: true), shrinking < widths.count {
|
|
20
|
-
let others = widths.indices.reduce(CGFloat(0)) { $1 == shrinking ? $0 : $0 + widths[$1] }
|
|
21
|
-
widths[shrinking] = max(0, min(widths[shrinking], line - others))
|
|
22
|
-
}
|
|
23
|
-
var overflow = widths.reduce(0, +) - line
|
|
24
|
-
var index = widths.count - 1
|
|
25
|
-
while overflow > 0, index >= 0 {
|
|
26
|
-
let cut = min(widths[index], overflow)
|
|
27
|
-
widths[index] -= cut
|
|
28
|
-
overflow -= cut
|
|
29
|
-
index -= 1
|
|
30
|
-
}
|
|
31
|
-
return widths
|
|
32
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import UIKit
|
|
2
|
-
|
|
3
|
-
/// Draws identity title runs on one line, each at the width that
|
|
4
|
-
/// `nativeListTitleSegmentWidths` allocates for the current bounds. Its
|
|
5
|
-
/// intrinsic width is the runs' natural width, so the title row stack can
|
|
6
|
-
/// give it less, and accessibility reads the row's `title` instead of runs.
|
|
7
|
-
final class NativeListTitleSegmentsLabel: UIView {
|
|
8
|
-
struct Run {
|
|
9
|
-
let text: String
|
|
10
|
-
let font: UIFont
|
|
11
|
-
let color: UIColor
|
|
12
|
-
let shrink: Bool
|
|
13
|
-
let maxWidthRatio: CGFloat?
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
private var runs: [Run] = []
|
|
17
|
-
private var naturalWidths: [CGFloat] = []
|
|
18
|
-
var lineHeight: CGFloat = 24 {
|
|
19
|
-
didSet {
|
|
20
|
-
invalidateIntrinsicContentSize()
|
|
21
|
-
setNeedsDisplay()
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
override init(frame: CGRect) {
|
|
26
|
-
super.init(frame: frame)
|
|
27
|
-
isOpaque = false
|
|
28
|
-
backgroundColor = .clear
|
|
29
|
-
contentMode = .redraw
|
|
30
|
-
isAccessibilityElement = true
|
|
31
|
-
accessibilityTraits = .staticText
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
35
|
-
|
|
36
|
-
func configure(_ runs: [Run], spokenText: String) {
|
|
37
|
-
self.runs = runs
|
|
38
|
-
naturalWidths = runs.map {
|
|
39
|
-
ceil(($0.text as NSString).size(withAttributes: [.font: $0.font]).width)
|
|
40
|
-
}
|
|
41
|
-
accessibilityLabel = spokenText.isEmpty ? nil : spokenText
|
|
42
|
-
invalidateIntrinsicContentSize()
|
|
43
|
-
setNeedsDisplay()
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
func reset() {
|
|
47
|
-
configure([], spokenText: "")
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
override var intrinsicContentSize: CGSize {
|
|
51
|
-
CGSize(width: naturalWidths.reduce(0, +), height: lineHeight)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
override func layoutSubviews() {
|
|
55
|
-
super.layoutSubviews()
|
|
56
|
-
setNeedsDisplay()
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
override func draw(_ rect: CGRect) {
|
|
60
|
-
guard !runs.isEmpty else { return }
|
|
61
|
-
let widths = nativeListTitleSegmentWidths(
|
|
62
|
-
natural: naturalWidths, shrink: runs.map(\.shrink),
|
|
63
|
-
maxWidthRatio: runs.map(\.maxWidthRatio), available: bounds.width)
|
|
64
|
-
let isRightToLeft = effectiveUserInterfaceLayoutDirection == .rightToLeft
|
|
65
|
-
var x: CGFloat = isRightToLeft ? bounds.width : 0
|
|
66
|
-
for (run, width) in zip(runs, widths) where width > 0 {
|
|
67
|
-
let paragraph = NSMutableParagraphStyle()
|
|
68
|
-
paragraph.lineBreakMode = .byTruncatingTail
|
|
69
|
-
let text = NSAttributedString(
|
|
70
|
-
string: run.text,
|
|
71
|
-
attributes: [.font: run.font, .foregroundColor: run.color, .paragraphStyle: paragraph])
|
|
72
|
-
let height = ceil(run.font.lineHeight)
|
|
73
|
-
let originX = isRightToLeft ? x - width : x
|
|
74
|
-
text.draw(
|
|
75
|
-
with: CGRect(x: originX, y: (bounds.height - height) / 2, width: width, height: height),
|
|
76
|
-
options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
|
|
77
|
-
x += isRightToLeft ? -width : width
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|