1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.text.android
18 
19 import android.text.TextDirectionHeuristic
20 import android.text.TextDirectionHeuristics
21 import android.text.TextPaint
22 import androidx.core.content.res.ResourcesCompat
23 import androidx.test.ext.junit.runners.AndroidJUnit4
24 import androidx.test.filters.SmallTest
25 import androidx.test.platform.app.InstrumentationRegistry
26 import androidx.testutils.fonts.R
27 import com.google.common.truth.Truth.assertThat
28 import org.junit.Test
29 import org.junit.runner.RunWith
30 
31 /**
32  * In this test cases, use following notations:
33  * - L1-LF shows an example strong LTR character.
34  * - R1-RF shows an example strong RTL character
35  */
36 @SmallTest
37 @OptIn(InternalPlatformTextApi::class)
38 @RunWith(AndroidJUnit4::class)
39 class LayoutGetHorizontalTest {
40 
41     private val sampleTypeface =
42         ResourcesCompat.getFont(
43             InstrumentationRegistry.getInstrumentation().targetContext,
44             R.font.sample_font
45         )
46 
47     private val LTR = TextDirectionHeuristics.LTR
48     private val RTL = TextDirectionHeuristics.RTL
49 
getUpstreamPrimaryHorizontalPositionnull50     private fun LayoutHelper.getUpstreamPrimaryHorizontalPosition(offset: Int) =
51         getHorizontalPosition(offset = offset, usePrimaryDirection = true, upstream = true)
52 
53     private fun LayoutHelper.getDownstreamPrimaryHorizontalPosition(offset: Int) =
54         getHorizontalPosition(offset = offset, usePrimaryDirection = true, upstream = false)
55 
56     private fun LayoutHelper.getUpstreamSecondaryHorizontalPosition(offset: Int) =
57         getHorizontalPosition(offset = offset, usePrimaryDirection = false, upstream = true)
58 
59     private fun LayoutHelper.getDownstreamSecondaryHorizontalPosition(offset: Int) =
60         getHorizontalPosition(offset = offset, usePrimaryDirection = false, upstream = false)
61 
62     private fun getLayout(
63         text: String,
64         fontSize: Int,
65         width: Int,
66         dir: TextDirectionHeuristic
67     ): LayoutHelper {
68         val paint =
69             TextPaint().apply {
70                 textSize = fontSize.toFloat()
71                 typeface = sampleTypeface
72             }
73         val layout =
74             StaticLayoutFactory.create(text = text, paint = paint, width = width, textDir = dir)
75         return LayoutHelper(layout)
76     }
77 
78     @Test
getHorizontal_LTRText_LTRParagraph_FirstCharacternull79     fun getHorizontal_LTRText_LTRParagraph_FirstCharacter() {
80         // The line break happens like as follows
81         //
82         // input (Logical): L1 L2 L3 L4 L5 L6 L7 L8 L9 LA LB LC
83         //
84         // |L1 L2 L3 L4 L5|
85         // |L6 L7 L8 L9 LA|
86         // |LB LC         |
87         //
88         val layout =
89             getLayout(
90                 text = "\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C",
91                 fontSize = 10,
92                 width = 50,
93                 dir = LTR
94             )
95 
96         val offset = 0 // Before L1
97         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
98         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
99         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
100         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
101     }
102 
103     @Test
getHorizontal_LTRText_LTRParagraph_LineBreakOffsetnull104     fun getHorizontal_LTRText_LTRParagraph_LineBreakOffset() {
105         // The line break happens like as follows
106         //
107         // input (Logical): L1 L2 L3 L4 L5 L6 L7 L8 L9 LA LB LC
108         //
109         // |L1 L2 L3 L4 L5|
110         // |L6 L7 L8 L9 LA|
111         // |LB LC         |
112         //
113         val layout =
114             getLayout(
115                 text = "\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C",
116                 fontSize = 10,
117                 width = 50,
118                 dir = LTR
119             )
120 
121         val offset = 5 // before L6
122         // If insert LX to first line, it will be |L1 L2 L3 L4 L5 LX|
123         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
124         // If insert RX to first line, it will be |L1 L2 L3 L4 L5 RX|
125         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
126         // If insert LX to second line, it will be |LX L6 L7 L8 L9 LA|
127         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
128         // If insert RX to second line, it will be |RX L6 L7 L8 L9 LA|
129         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
130     }
131 
132     @Test
getHorizontal_LTRText_LTRParagraph_LastOffsetnull133     fun getHorizontal_LTRText_LTRParagraph_LastOffset() {
134         // The line break happens like as follows
135         //
136         // input (Logical): L1 L2 L3 L4 L5 L6 L7 L8 L9 LA LB LC
137         //
138         // |L1 L2 L3 L4 L5|
139         // |L6 L7 L8 L9 LA|
140         // |LB LC         |
141         //
142         val layout =
143             getLayout(
144                 text = "\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C",
145                 fontSize = 10,
146                 width = 50,
147                 dir = LTR
148             )
149 
150         val offset = layout.layout.text.length // after LC
151         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(20)
152         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(20)
153         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(20)
154         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(20)
155     }
156 
157     @Test
getHorizontal_LTRText_LTRParagraph_Othernull158     fun getHorizontal_LTRText_LTRParagraph_Other() {
159         // The line break happens like as follows
160         //
161         // input (Logical): L1 L2 L3 L4 L5 L6 L7 L8 L9 LA LB LC
162         //
163         // |L1 L2 L3 L4 L5|
164         // |L6 L7 L8 L9 LA|
165         // |LB LC         |
166         //
167         val layout =
168             getLayout(
169                 text = "\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C",
170                 fontSize = 10,
171                 width = 50,
172                 dir = LTR
173             )
174 
175         val offset = 7 // before L8
176         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(20)
177         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(20)
178         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(20)
179         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(20)
180     }
181 
182     @Test
getHorizontal_LTRText_RTLParagraph_FirstCharacternull183     fun getHorizontal_LTRText_RTLParagraph_FirstCharacter() {
184         // The line break happens like as follows
185         //
186         // input (Logical): L1 L2 L3 L4 L5 L6 L7 L8 L9 LA LB LC
187         //
188         // |L1 L2 L3 L4 L5|
189         // |L6 L7 L8 L9 LA|
190         // |         LB LC|
191         //
192         val layout =
193             getLayout(
194                 text = "\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C",
195                 fontSize = 10,
196                 width = 50,
197                 dir = RTL
198             )
199 
200         val offset = 0 // before L1
201         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
202         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
203         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
204         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
205     }
206 
207     @Test
getHorizontal_LTRText_RTLParagraph_LineBreakOffsetnull208     fun getHorizontal_LTRText_RTLParagraph_LineBreakOffset() {
209         // The line break happens like as follows
210         //
211         // input (Logical): L1 L2 L3 L4 L5 L6 L7 L8 L9 LA LB LC
212         //
213         // |L1 L2 L3 L4 L5|
214         // |L6 L7 L8 L9 LA|
215         // |         LB LC|
216         //
217         val layout =
218             getLayout(
219                 text = "\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C",
220                 fontSize = 10,
221                 width = 50,
222                 dir = RTL
223             )
224 
225         val offset = 5 // before L6 == after L5
226         // If insert RX to the first line, it will be |RX L1 L2 L3 L4 L5|
227         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
228         // If insert LX to the first line, it will be |L1 L2 L3 L4 L5 LX|
229         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
230         // If insert RX to the second line, it will be |L6 L7 L8 L9 LA RX|
231         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
232         // If insert LX to the second line, it will be |LX L6 L7 L8 L9 LA|
233         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
234     }
235 
236     @Test
getHorizontal_LTRText_RTLParagraph_LastOffsetnull237     fun getHorizontal_LTRText_RTLParagraph_LastOffset() {
238         // The line break happens like as follows
239         //
240         // input (Logical): L1 L2 L3 L4 L5 L6 L7 L8 L9 LA LB LC
241         //
242         // |L1 L2 L3 L4 L5|
243         // |L6 L7 L8 L9 LA|
244         // |         LB LC|
245         //
246         val layout =
247             getLayout(
248                 text = "\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C",
249                 fontSize = 10,
250                 width = 50,
251                 dir = RTL
252             )
253 
254         val offset = layout.layout.text.length // after LC
255         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(30)
256         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
257         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(30)
258         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
259     }
260 
261     @Test
getHorizontal_LTRText_RTLParagraph_Othernull262     fun getHorizontal_LTRText_RTLParagraph_Other() {
263         // The line break happens like as follows
264         //
265         // input (Logical): L1 L2 L3 L4 L5 L6 L7 L8 L9 LA LB LC
266         //
267         // |L1 L2 L3 L4 L5|
268         // |L6 L7 L8 L9 LA|
269         // |         LB LC|
270         //
271         val layout =
272             getLayout(
273                 text = "\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C",
274                 fontSize = 10,
275                 width = 50,
276                 dir = RTL
277             )
278 
279         val offset = 7 // before L8
280         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(20)
281         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(20)
282         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(20)
283         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(20)
284     }
285 
286     @Test
getHorizontal_RTLText_RTLParagraph_FirstCharacternull287     fun getHorizontal_RTLText_RTLParagraph_FirstCharacter() {
288         // The line break happens like as follows
289         //
290         // input (Logical): R1 R2 R3 R4 R5 R6 R7 R8 R9 RA RB RC
291         //
292         // |R5 R4 R3 R2 R1|
293         // |RA R9 R8 R7 R6|
294         // |         RC RB|
295         //
296         val layout =
297             getLayout(
298                 text = "\u05D1\u05D2\u05D3\u05D4\u05D5\u05D6\u05D7\u05D8\u05D9\u05DA\u05DB\u05DC",
299                 fontSize = 10,
300                 width = 50,
301                 dir = RTL
302             )
303 
304         val offset = 0 // before R1
305         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
306         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
307         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
308         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
309     }
310 
311     @Test
getHorizontal_RTLText_RTLParagraph_LineBreakOffsetnull312     fun getHorizontal_RTLText_RTLParagraph_LineBreakOffset() {
313         // The line break happens like as follows
314         //
315         // input (Logical): R1 R2 R3 R4 R5 R6 R7 R8 R9 RA RB RC
316         //
317         // |R5 R4 R3 R2 R1|
318         // |RA R9 R8 R7 R6|
319         // |         RC RB|
320         //
321         val layout =
322             getLayout(
323                 text = "\u05D1\u05D2\u05D3\u05D4\u05D5\u05D6\u05D7\u05D8\u05D9\u05DA\u05DB\u05DC",
324                 fontSize = 10,
325                 width = 50,
326                 dir = RTL
327             )
328 
329         val offset = 5 // before R6 == after R5
330         // If insert RX to the first line, it will be |RX R5 R4 R3 R2 R1|
331         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
332         // If insert LX to the first line, it will be |LX R5 R4 R3 R2 R1|
333         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
334         // If insert RX to the second line, it will be |RA R9 R8 R7 R6 RX|
335         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
336         // If insert LX to the second line, it will be |RA R9 R8 R7 R6 LX|
337         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
338     }
339 
340     @Test
getHorizontal_RTLText_RTLParagraph_LastCharacternull341     fun getHorizontal_RTLText_RTLParagraph_LastCharacter() {
342         // The line break happens like as follows
343         //
344         // input (Logical): R1 R2 R3 R4 R5 R6 R7 R8 R9 RA RB RC
345         //
346         // |R5 R4 R3 R2 R1|
347         // |RA R9 R8 R7 R6|
348         // |         RC RB|
349         //
350         val layout =
351             getLayout(
352                 text = "\u05D1\u05D2\u05D3\u05D4\u05D5\u05D6\u05D7\u05D8\u05D9\u05DA\u05DB\u05DC",
353                 fontSize = 10,
354                 width = 50,
355                 dir = RTL
356             )
357 
358         val offset = layout.layout.text.length // after RC
359         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(30)
360         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(30)
361         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(30)
362         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(30)
363     }
364 
365     @Test
getHorizontal_RTLText_RTLParagraph_Othernull366     fun getHorizontal_RTLText_RTLParagraph_Other() {
367         // The line break happens like as follows
368         //
369         // input (Logical): R1 R2 R3 R4 R5 R6 R7 R8 R9 RA RB RC
370         //
371         // |R5 R4 R3 R2 R1|
372         // |RA R9 R8 R7 R6|
373         // |         RC RB|
374         //
375         val layout =
376             getLayout(
377                 text = "\u05D1\u05D2\u05D3\u05D4\u05D5\u05D6\u05D7\u05D8\u05D9\u05DA\u05DB\u05DC",
378                 fontSize = 10,
379                 width = 50,
380                 dir = RTL
381             )
382 
383         val offset = 7 // before R8
384         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(30)
385         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(30)
386         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(30)
387         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(30)
388     }
389 
390     @Test
getHorizontal_RTLText_LTRParagraph_FirstCharacternull391     fun getHorizontal_RTLText_LTRParagraph_FirstCharacter() {
392         // The line break happens like as follows
393         //
394         // input (Logical): R1 R2 R3 R4 R5 R6 R7 R8 R9 RA RB RC
395         //
396         // |R5 R4 R3 R2 R1|
397         // |RA R9 R8 R7 R6|
398         // |RC RB         |
399         //
400         val layout =
401             getLayout(
402                 text = "\u05D1\u05D2\u05D3\u05D4\u05D5\u05D6\u05D7\u05D8\u05D9\u05DA\u05DB\u05DC",
403                 fontSize = 10,
404                 width = 50,
405                 dir = LTR
406             )
407 
408         val offset = 0 // before R1
409         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
410         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
411         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
412         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
413     }
414 
415     @Test
getHorizontal_RTLText_LTRParagraph_LineBreakOffsetnull416     fun getHorizontal_RTLText_LTRParagraph_LineBreakOffset() {
417         // The line break happens like as follows
418         //
419         // input (Logical): R1 R2 R3 R4 R5 R6 R7 R8 R9 RA RB RC
420         //
421         // |R5 R4 R3 R2 R1|
422         // |RA R9 R8 R7 R6|
423         // |RC RB         |
424         //
425         val layout =
426             getLayout(
427                 text = "\u05D1\u05D2\u05D3\u05D4\u05D5\u05D6\u05D7\u05D8\u05D9\u05DA\u05DB\u05DC",
428                 fontSize = 10,
429                 width = 50,
430                 dir = LTR
431             )
432 
433         val offset = 5 // befroe R6 == after R5
434         // If insert LX to the first line, it will be |R5 R4 R3 R2 R1 LX|
435         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
436         // If insert RX to the first line, it will be |RX R5 R4 R3 R2 R1|
437         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
438         // If insert LX to the second line, it will be |LX RA R9 R8 R7 R6|
439         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
440         // If insert RX to the second line, it will be |RA R9 R8 R7 R6 RX|
441         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
442     }
443 
444     @Test
getHorizontal_RTLText_LTRParagraph_LastCharacternull445     fun getHorizontal_RTLText_LTRParagraph_LastCharacter() {
446         // The line break happens like as follows
447         //
448         // input (Logical): R1 R2 R3 R4 R5 R6 R7 R8 R9 RA RB RC
449         //
450         // |R5 R4 R3 R2 R1|
451         // |RA R9 R8 R7 R6|
452         // |RC RB         |
453         //
454         val layout =
455             getLayout(
456                 text = "\u05D1\u05D2\u05D3\u05D4\u05D5\u05D6\u05D7\u05D8\u05D9\u05DA\u05DB\u05DC",
457                 fontSize = 10,
458                 width = 50,
459                 dir = LTR
460             )
461 
462         val offset = layout.layout.text.length // after RB
463         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(20)
464         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
465         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(20)
466         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
467     }
468 
469     @Test
getHorizontal_RTLText_LTRParagraph_Othernull470     fun getHorizontal_RTLText_LTRParagraph_Other() {
471         // The line break happens like as follows
472         //
473         // input (Logical): R1 R2 R3 R4 R5 R6 R7 R8 R9 RA RB RC
474         //
475         // |R5 R4 R3 R2 R1|
476         // |RA R9 R8 R7 R6|
477         // |RC RB         |
478         //
479         val layout =
480             getLayout(
481                 text = "\u05D1\u05D2\u05D3\u05D4\u05D5\u05D6\u05D7\u05D8\u05D9\u05DA\u05DB\u05DC",
482                 fontSize = 10,
483                 width = 50,
484                 dir = LTR
485             )
486 
487         val offset = 7 // before R8
488         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(30)
489         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(30)
490         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(30)
491         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(30)
492     }
493 
494     @Test
getHorizontal_BidiText_BiDiTransitionNotLineBreakOffset_FromLTRToRTL_LTRParagraphnull495     fun getHorizontal_BidiText_BiDiTransitionNotLineBreakOffset_FromLTRToRTL_LTRParagraph() {
496         // The line break happens like as follows
497         //
498         // input (Logical): L1 R1 R2 R3 L2 L3 L5 L6 L7 L8
499         //
500         // |L1 R3 R2 R1 L2|
501         // |L3 L4 L5 L6 L7|
502         // |L8 L9         |
503         //
504         val layout =
505             getLayout(
506                 text = "\u0061\u05D1\u05D2\u05D3\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C",
507                 fontSize = 10,
508                 width = 50,
509                 dir = LTR
510             )
511 
512         val offset = 1 // before R1
513         // If insert LX to first line, it will be |L1 LX R3 R2 R1 L2|
514         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(10)
515         // If insert RX to first line, it will be |L1 R3 R2 R1 RX L2|
516         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(40)
517         // If insert LX to first line, it will be |L1 LX R3 R2 R1 L2|
518         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(10)
519         // If insert RX to first line, it will be |L1 R3 R2 R1 RX L2|
520         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(40)
521     }
522 
523     @Test
getHorizontal_BidiText_BiDiTransitionNotLineBreakOffset_FromLTRToRTL_RTLParagraphnull524     fun getHorizontal_BidiText_BiDiTransitionNotLineBreakOffset_FromLTRToRTL_RTLParagraph() {
525         // The line break happens like as follows
526         //
527         // input (Logical): L1 R1 R2 R3 L2 L3 L5 L6 L7 L8 L9
528         //
529         // |L2 R3 R2 R1 L1|
530         // |L3 L4 L5 L6 L7|
531         // |         L8 L9|
532         //
533         val layout =
534             getLayout(
535                 text = "\u0061\u05D1\u05D2\u05D3\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C",
536                 fontSize = 10,
537                 width = 50,
538                 dir = RTL
539             )
540 
541         val offset = 1 // before R1
542         // If insert RX to first line, it will be |L2 R3 R2 R1 RX L1|
543         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(40)
544         // If insert LX to first line, it will be |L2 R3 R2 R1 L1 LX|
545         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
546         // If insert RX to first line, it will be |L2 R3 R2 R1 RX L1|
547         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(40)
548         // If insert LX to first line, it will be |L2 R3 R2 R1 L1 LX|
549         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
550     }
551 
552     @Test
getHorizontal_BidiText_BiDiTransitionNotLineBreakOffset_FromRTLtoLTR_RTLParagraphnull553     fun getHorizontal_BidiText_BiDiTransitionNotLineBreakOffset_FromRTLtoLTR_RTLParagraph() {
554         // The line break happens like as follows
555         //
556         // input (Logical): R1 L1 L2 L3 R2 R3 R4 R5 R6 R7 R8 R9
557         //
558         // |R2 L1 L2 L3 R1|
559         // |R7 R6 R5 R4 R3|
560         // |         R9 R8|
561         //
562         val layout =
563             getLayout(
564                 text = "\u05D0\u0061\u0062\u0063\u05D4\u05D5\u05D6\u05D7\u05D8\u05DA\u05DB\u05DC",
565                 fontSize = 10,
566                 width = 50,
567                 dir = RTL
568             )
569 
570         val offset = 1 // before L1
571         // If insert RX to first line, it will be |R2 L1 L2 L3 RX R1|
572         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(40)
573         // If insert LX to first line, it will be |R2 LX L1 L2 L3 R1|
574         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(10)
575         // If insert RX to first line, it will be |R2 L1 L2 L3 RX R1|
576         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(40)
577         // If insert LX to first line, it will be |R2 LX L1 L2 L3 R1|
578         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(10)
579     }
580 
581     @Test
getHorizontal_BidiText_BiDiTransitionNotLineBreakOffset_FromRTLtoLTR_LTRParagraphnull582     fun getHorizontal_BidiText_BiDiTransitionNotLineBreakOffset_FromRTLtoLTR_LTRParagraph() {
583         // The line break happens like as follows
584         //
585         // input (Logical): R1 L1 L2 L3 R2 R3 R4 R5 R6 R7 R8 R9
586         //
587         // |R1 L1 L2 L3 R2|
588         // |R7 R6 R5 R4 R3|
589         // |R9 R8         |
590         //
591         val layout =
592             getLayout(
593                 text = "\u05D0\u0061\u0062\u0063\u05D4\u05D5\u05D6\u05D7\u05D8\u05DA\u05DB\u05DC",
594                 fontSize = 10,
595                 width = 50,
596                 dir = LTR
597             )
598 
599         val offset = 1 // before L1
600         // If insert LX to first line, it will be |R2 LX L1 L2 L3 R1|
601         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(10)
602         // If insert RX to first line, it will be |RX R1 L1 L2 L3 R2|
603         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
604         // If insert LX to first line, it will be |R2 LX L1 L2 L3 R1|
605         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(10)
606         // If insert RX to first line, it will be |RX R1 L1 L2 L3 R2|
607         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
608     }
609 
610     @Test
getHorizontal_BidiText_BiDiTransitionLineBreakOffset_FromLTRToRTL_LTRParagraphnull611     fun getHorizontal_BidiText_BiDiTransitionLineBreakOffset_FromLTRToRTL_LTRParagraph() {
612         // The line break happens like as follows
613         //
614         // input (Logical): L1 L2 L3 L4 L5 R1 R2 R3 R4 R5 R6 R7
615         //
616         // |L1 L2 L3 L4 L5|
617         // |R5 R4 R3 R2 R1|
618         // |R7 R6         |
619         //
620         val layout =
621             getLayout(
622                 text = "\u0061\u0062\u0063\u0064\u0065\u05D5\u05D6\u05D7\u05D8\u05DA\u05DB\u05DC",
623                 fontSize = 10,
624                 width = 50,
625                 dir = LTR
626             )
627 
628         val offset = 5 // before R1 == after L5
629         // If insert LX to first line, it will be |L1 L2 L3 L4 L5 LX|
630         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
631         // If insert RX to first line, it will be |L1 L2 L3 L4 L5 RX|
632         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
633         // If insert LX to second line, it will be |L1 R5 R4 R3 R2 R1|
634         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
635         // If insert RX to second line, it will be |R5 R4 R3 R2 R1 RX|
636         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
637     }
638 
639     @Test
getHorizontal_BidiText_BiDiTransitionLineBreakOffset_FromLTRToRTL_RTLParagraphnull640     fun getHorizontal_BidiText_BiDiTransitionLineBreakOffset_FromLTRToRTL_RTLParagraph() {
641         // The line break happens like as follows
642         //
643         // input (Logical): L1 L2 L3 L4 L5 R1 R2 R3 R4 R5 R6 R7
644         //
645         // |L1 L2 L3 L4 L5|
646         // |R5 R4 R3 R2 R1|
647         // |         R7 R6|
648         //
649         val layout =
650             getLayout(
651                 text = "\u0061\u0062\u0063\u0064\u0065\u05D5\u05D6\u05D7\u05D8\u05DA\u05DB\u05DC",
652                 fontSize = 10,
653                 width = 50,
654                 dir = RTL
655             )
656 
657         val offset = 5 // before R1 == after L5
658         // If insert RX to first line, it will be |RX L1 L2 L3 L4 L5|
659         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
660         // If insert LX to first line, it will be |L1 L2 L3 L4 L5 LX|
661         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
662         // If insert RX to second line, it will be |R5 R4 R3 R2 R1 RX|
663         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
664         // If insert LX to second line, it will be |R5 R4 R3 R2 R1 LX|
665         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
666     }
667 
668     @Test
getHorizontal_BidiText_BiDiTransitionLineBreakOffset_FromRTLToLTR_RTLParagraphnull669     fun getHorizontal_BidiText_BiDiTransitionLineBreakOffset_FromRTLToLTR_RTLParagraph() {
670         // The line break happens like as follows
671         //
672         // input (Logical): R1 R2 R3 R4 R5 L1 L2 L3 L4 L5 L6 L7
673         //
674         // |R5 R4 R3 R2 R1|
675         // |L1 L2 L3 L4 L5|
676         // |         L6 L7|
677         //
678         val layout =
679             getLayout(
680                 text = "\u05D0\u05D1\u05D2\u05D3\u05D4\u0061\u0062\u0063\u0064\u0065\u0066\u0067",
681                 fontSize = 10,
682                 width = 50,
683                 dir = RTL
684             )
685 
686         val offset = 5 // before L1 == after R5
687         // If insert RX to first line, it will be |RX R5 R4 R3 R2 R1|
688         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
689         // If insert LX to first line, it will be |LX R5 R4 R3 R2 R1|
690         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
691         // If insert RX to second line, it will be |L1 L2 L3 L4 L5 RX|
692         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
693         // If insert LX to second line, it will be |LX L1 L2 L3 L4 L5|
694         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
695     }
696 
697     @Test
getHorizontal_BidiText_BiDiTransitionLineBreakOffset_FromRTLToLTR_LTRParagraphnull698     fun getHorizontal_BidiText_BiDiTransitionLineBreakOffset_FromRTLToLTR_LTRParagraph() {
699         // The line break happens like as follows
700         //
701         // input (Logical): R1 R2 R3 R4 R5 L1 L2 L3 L4 L5 L6 L7
702         //
703         // |R5 R4 R3 R2 R1|
704         // |L1 L2 L3 L4 L5|
705         // |L6 L7         |
706         //
707         val layout =
708             getLayout(
709                 text = "\u05D0\u05D1\u05D2\u05D3\u05D4\u0061\u0062\u0063\u0064\u0065\u0066\u0067",
710                 fontSize = 10,
711                 width = 50,
712                 dir = LTR
713             )
714 
715         val offset = 5 // before L1 == after R5
716         // If insert LX to first line, it will be |R5 R4 R3 R2 R1 LX|
717         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
718         // If insert RX to first line, it will be |RX R5 R4 R3 R2 R1|
719         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
720         // If insert LX to second line, it will be |LX L1 L2 L3 L4 L5|
721         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
722         // If insert RX to second line, it will be |R1 L1 L2 L3 L4 L5|
723         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
724     }
725 
726     @Test
getHorizontal_BidiText_LineBreakOffset_FromMiddle_FromLTRToRTL_LTRParagraphnull727     fun getHorizontal_BidiText_LineBreakOffset_FromMiddle_FromLTRToRTL_LTRParagraph() {
728         // The line break happens like as follows
729         //
730         // input (Logical): L1 L2 R1 R2 R3 R4 R5 R6 R7 R8 R9 RA
731         //
732         // |L1 L2 R3 R2 R1|
733         // |R8 R7 R6 R5 R4|
734         // |RA R9         |
735         //
736         val layout =
737             getLayout(
738                 "\u0061\u0062\u05D0\u05D1\u05D2\u05D3\u05D4\u05D5\u05D6\u05D7\u05D8\u05DA\u05DB",
739                 10,
740                 50,
741                 LTR
742             )
743 
744         val offset = 5 // before R4 == after R3
745         // If insert LX to first line, it will be |L1 L2 R3 R2 R1 LX|
746         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
747         // If insert RX to first line, it will be |L1 L2 RX R3 R2 R1|
748         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(20)
749         // If insert LX to second line, it will be |LX R5 R7 R6 R5 R4|
750         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
751         // If insert RX to second line, it will be |R5 R7 R6 R5 R4 RX|
752         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
753     }
754 
755     @Test
getHorizontal_BidiText_LineBreakOffset_FromMiddle_FromLTRToRTL_RTLParagraphnull756     fun getHorizontal_BidiText_LineBreakOffset_FromMiddle_FromLTRToRTL_RTLParagraph() {
757         // The line break happens like as follows
758         //
759         // input (Logical): L1 L2 R1 R2 R3 R4 R5 R6 R7 R8 R9 RA
760         //
761         // |R3 R2 R1 L1 L2|
762         // |R8 R7 R6 R5 R4|
763         // |         RA R9|
764         //
765         val layout =
766             getLayout(
767                 "\u0061\u0062\u05D0\u05D1\u05D2\u05D3\u05D4\u05D5\u05D6\u05D7\u05D8\u05DA",
768                 10,
769                 50,
770                 RTL
771             )
772 
773         val offset = 5 // before R4 == after R3
774         // If insert RX to first line, it will be |RX R3 R2 R1 L1 L2|
775         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
776         // If insert LX to first line, it will be |LX R3 R2 R1 L1 L2|
777         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
778         // If insert RX to second line, it will be |R8 R7 R6 R5 R4 RX|
779         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
780         // If insert LX to second line, it will be |R8 R7 R6 R5 R4 LX|
781         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
782     }
783 
784     @Test
getHorizontal_BidiText_LineBreakOffset_FromMiddle_FromRTLToLTR_LTRParagraphnull785     fun getHorizontal_BidiText_LineBreakOffset_FromMiddle_FromRTLToLTR_LTRParagraph() {
786         // The line break happens like as follows
787         //
788         // input (Logical): R1 R2 L1 L2 L3 L4 L5 L6 L7 L8 L9 LA
789         //
790         // |R2 R1 L1 L2 L3|
791         // |L4 L5 L6 L7 L8|
792         // |L9 RA         |
793         //
794         val layout =
795             getLayout(
796                 "\u05D0\u05D1\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A",
797                 10,
798                 50,
799                 LTR
800             )
801 
802         val offset = 5 // before L4 == after L3
803         // If insert LX to first line, it will be |R2 R1 L1 L2 L3 LX|
804         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
805         // If insert RX to first line, it will be |R2 R1 L1 L2 L3 RX|
806         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
807         // If insert LX to second line, it will be |LX L4 L5 L6 L7 L8|
808         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
809         // If insert RX to second line, it will be |RX L4 L5 L6 L7 L8|
810         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
811     }
812 
813     @Test
getHorizontal_BidiText_LineBreakOffset_FromMiddle_FromRTLToLTR_RTLParagraphnull814     fun getHorizontal_BidiText_LineBreakOffset_FromMiddle_FromRTLToLTR_RTLParagraph() {
815         // The line break happens like as follows
816         //
817         // input (Logical): R1 R2 L1 L2 L3 L4 L5 L6 L7 L8 L9 LA
818         //
819         // |L1 L2 L3 R2 R1|
820         // |L4 L5 L6 L7 L8|
821         // |         L9 RA|
822         //
823         val layout =
824             getLayout(
825                 "\u05D0\u05D1\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A",
826                 10,
827                 50,
828                 RTL
829             )
830 
831         val offset = 5 // before L4 == after L3
832         // If insert RX to first line, it will be |RX L1 L2 L3 R2 R1|
833         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
834         // If insert LX to first line, it will be |L1 L2 L3 L4 R2 R1|
835         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(30)
836         // If insert RX to second line, it will be |L4 L5 L6 L7 L8 RX|
837         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
838         // If insert LX to second line, it will be |LX L4 L5 L6 L7 L8|
839         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
840     }
841 
842     @Test
getHorizontal_BidiText_LineBreakOffset_ToMiddle_FromLTRToRTL_LTRParagraphnull843     fun getHorizontal_BidiText_LineBreakOffset_ToMiddle_FromLTRToRTL_LTRParagraph() {
844         // The line break happens like as follows
845         //
846         // input (Logical): L1 L2 L3 L4 L5 L6 L7 R1 R2 R3 R4 R5
847         //
848         // |L1 L2 L3 L4 L5|
849         // |L6 L7 R3 R2 R1|
850         // |R5 R4         |
851         //
852         val layout =
853             getLayout(
854                 "\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u05D0\u05D1\u05D2\u05D3\u05D4",
855                 10,
856                 50,
857                 LTR
858             )
859 
860         val offset = 5 // before L6 == after L5
861         // If insert LX to first line, it will be |L1 L2 L3 L4 L5 LX|
862         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
863         // If insert RX to first line, it will be |L1 L2 L3 L4 L5 RX|
864         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
865         // If insert LX to second line, it will be |LX L6 L7 R3 R2 R1|
866         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
867         // If insert RX to second line, it will be |RX L6 L7 R3 R2 R1|
868         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
869     }
870 
871     @Test
getHorizontal_BidiText_LineBreakOffset_ToMiddle_FromLTRToRTL_RTLParagraphnull872     fun getHorizontal_BidiText_LineBreakOffset_ToMiddle_FromLTRToRTL_RTLParagraph() {
873         // The line break happens like as follows
874         //
875         // input (Logical): L1 L2 L3 L4 L5 L6 L7 R1 R2 R3 R4 R5
876         //
877         // |L1 L2 L3 L4 L5|
878         // |R3 R2 R1 L6 L7|
879         // |         R5 R4|
880         //
881         val layout =
882             getLayout(
883                 "\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u05D0\u05D1\u05D2\u05D3\u05D4",
884                 10,
885                 50,
886                 RTL
887             )
888 
889         val offset = 5 // before L6 == after L5
890         // If insert RX to first line, it will be |RX L1 L2 L3 L4 L5|
891         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
892         // If insert LX to first line, it will be |L1 L2 L3 L4 L5 LX|
893         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
894         // If insert RX to second line, it will be |R3 R2 R1 L6 L7 RX|
895         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
896         // If insert LX to second line, it will be |R3 R2 R1 LX L6 L7|
897         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(30)
898     }
899 
900     @Test
getHorizontal_BidiText_LineBreakOffset_ToMiddle_FromRTLToLTR_LTRParagraphnull901     fun getHorizontal_BidiText_LineBreakOffset_ToMiddle_FromRTLToLTR_LTRParagraph() {
902         // The line break happens like as follows
903         //
904         // input (Logical): R1 R2 R3 R4 R5 R6 R7 L1 L2 L3 L4 L5
905         //
906         // |R5 R4 R3 R2 R1|
907         // |R7 R6 L1 L2 L3|
908         // |L4 L5         |
909         //
910         val layout =
911             getLayout(
912                 "\u05D0\u05D1\u05D2\u05D3\u05D4\u05D5\u05D6\u0061\u0062\u0063\u0064\u0065",
913                 10,
914                 50,
915                 LTR
916             )
917 
918         val offset = 5 // before R6 == after R5
919         // If insert LX to first line, it will be |R5 R4 R3 R2 R1 LX|
920         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
921         // If insert RX to first line, it will be |RX R5 R4 R3 R2 R1|
922         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
923         // If insert LX to second line, it will be |LX R7 R6 L1 L2 L3|
924         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
925         // If insert RX to second line, it will be |R7 R6 RX L1 L2 L3|
926         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(20)
927     }
928 
929     @Test
getHorizontal_BidiText_LineBreakOffset_ToMiddle_FromRTLToLTR_RTLParagraphnull930     fun getHorizontal_BidiText_LineBreakOffset_ToMiddle_FromRTLToLTR_RTLParagraph() {
931         // The line break happens like as follows
932         //
933         // input (Logical): R1 R2 R3 R4 R5 R6 R7 L1 L2 L3 L4 L5
934         //
935         // |R5 R4 R3 R2 R1|
936         // |L1 L2 L3 R7 R6|
937         // |         L4 L5|
938         //
939         val layout =
940             getLayout(
941                 "\u05D0\u05D1\u05D2\u05D3\u05D4\u05D5\u05D6\u0061\u0062\u0063\u0064\u0065",
942                 10,
943                 50,
944                 RTL
945             )
946 
947         val offset = 5 // before R6 == after R5
948         // If insert RX to first line, it will be |RX R5 R4 R3 R2 R1|
949         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
950         // If insert LX to first line, it will be |LX R5 R4 R3 R2 R1|
951         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
952         // If insert RX to second line, it will be |L1 L2 L3 R7 R6 RX|
953         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
954         // If insert LX to second line, it will be |L1 L2 L3 R7 R6 LX|
955         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
956     }
957 
958     @Test
getHorizontal_BiDi_Whitspacenull959     fun getHorizontal_BiDi_Whitspace() {
960         // The line break happens like as follows
961         //
962         // input (Logical): R1 R2 SP R3 R4 R6 SP R7 R8 SP R9 RA
963         //
964         // |R4 R3 SP R2 R1| (SP)
965         // |L1 L2 SP L3 L4| (SP)
966         // |L5 L6         |
967         val layout =
968             getLayout(
969                 "\u05D0\u05D1 \u05D2\u05D3 \u0061\u0062 \u0063\u0064 \u0065\u0066",
970                 10,
971                 50,
972                 LTR
973             )
974 
975         val offset = 6 // before L1 == after SP
976         // If insert LX to first line, it will be |R4 R3 SP R2 R1 SP LX|
977         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
978         // If insert RX to first line, it will be |RX SP R4 R3 SP R2 R1|
979         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
980         // If insert LX to second line, it will be |LX L2 SP L3 L4| (SP)
981         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
982         // If insert RX to second line, it will be |RX L1 L2 SP L3 L4| (SP)
983         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
984     }
985 
986     @Test
getHorizontal_BiDi_singleLine_Whitespacenull987     fun getHorizontal_BiDi_singleLine_Whitespace() {
988         // The line break happens like as follows
989         //
990         // input (Logical): R1 R2 SP L1 L2 SP L3 L4 SP R3 R4 SP L5 L6
991         //
992         // |L1 L2 SP R2 R1| (SP)
993         // |L3 L4 SP R4 R3| (SP)
994         // |L5 L6         |
995         val layout =
996             getLayout(
997                 "\u05D0\u05D1 \u0061\u0062 \u0063\u0064 \u05D3\u05D4 \u0065\u0066",
998                 10,
999                 50,
1000                 LTR
1001             )
1002 
1003         val offset = 6 // before L3 == after SP
1004         // If insert LX to first line, it will be |L1 L2 SP R2 R1 SP LX|
1005         assertThat(layout.getUpstreamPrimaryHorizontalPosition(offset)).isEqualTo(50)
1006         // If insert RX to first line, it will be |L1 L2 SP R2 R1 SP RX|
1007         assertThat(layout.getUpstreamSecondaryHorizontalPosition(offset)).isEqualTo(50)
1008         // If insert LX to second line, it will be |LX L3 L4 SP R4 R3| (SP)
1009         assertThat(layout.getDownstreamPrimaryHorizontalPosition(offset)).isEqualTo(0)
1010         // If insert RX to second line, it will be |RX L3 L4 SP R4 R3| (SP)
1011         assertThat(layout.getDownstreamSecondaryHorizontalPosition(offset)).isEqualTo(0)
1012     }
1013 }
1014