Browse Source

LibWeb: Layout text chunks based on their Unicode direction

Append text chunks to either the start or end of the text fragment,
depending on the text direction. The direction is determined by what
script its code points are from.
BenJilks 10 months ago
parent
commit
11e7d72686

+ 118 - 94
Tests/LibWeb/Layout/expected/block-and-inline/float-left-and-right-with-justified-text-in-between.txt

@@ -15,168 +15,192 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
           " "
       frag 6 from TextNode start: 19, length: 3, rect: [763.59375,10 25.96875x22] baseline: 17
           "sit"
-      frag 7 from TextNode start: 23, length: 5, rect: [554,32 52.734375x22] baseline: 17
-          "amet,"
-      frag 8 from TextNode start: 28, length: 1, rect: [607,32 62.1875x22] baseline: 17
+      frag 7 from TextNode start: 23, length: 4, rect: [554,32 46.421875x22] baseline: 17
+          "amet"
+      frag 8 from TextNode start: 27, length: 1, rect: [600,32 6.3125x22] baseline: 17
+          ","
+      frag 9 from TextNode start: 28, length: 1, rect: [607,32 62.1875x22] baseline: 17
           " "
-      frag 9 from TextNode start: 29, length: 11, rect: [669.1875,32 121.078125x22] baseline: 17
+      frag 10 from TextNode start: 29, length: 11, rect: [669.1875,32 121.078125x22] baseline: 17
           "consectetur"
-      frag 10 from TextNode start: 41, length: 10, rect: [554,54 94.671875x22] baseline: 17
+      frag 11 from TextNode start: 41, length: 10, rect: [554,54 94.671875x22] baseline: 17
           "adipiscing"
-      frag 11 from TextNode start: 51, length: 1, rect: [649,54 105.40625x22] baseline: 17
+      frag 12 from TextNode start: 51, length: 1, rect: [649,54 105.40625x22] baseline: 17
           " "
-      frag 12 from TextNode start: 52, length: 5, rect: [754.40625,54 35.921875x22] baseline: 17
-          "elit."
-      frag 13 from TextNode start: 58, length: 11, rect: [554,76 123.375x22] baseline: 17
+      frag 13 from TextNode start: 52, length: 4, rect: [754.40625,54 30.484375x22] baseline: 17
+          "elit"
+      frag 14 from TextNode start: 56, length: 1, rect: [784.40625,54 5.4375x22] baseline: 17
+          "."
+      frag 15 from TextNode start: 58, length: 11, rect: [554,76 123.375x22] baseline: 17
           "Suspendisse"
-      frag 14 from TextNode start: 69, length: 1, rect: [677,76 100.9375x22] baseline: 17
+      frag 16 from TextNode start: 69, length: 1, rect: [677,76 100.9375x22] baseline: 17
           " "
-      frag 15 from TextNode start: 70, length: 1, rect: [777.9375,76 11.6875x22] baseline: 17
+      frag 17 from TextNode start: 70, length: 1, rect: [777.9375,76 11.6875x22] baseline: 17
           "a"
-      frag 16 from TextNode start: 72, length: 8, rect: [554,98 82.078125x22] baseline: 17
+      frag 18 from TextNode start: 72, length: 8, rect: [554,98 82.078125x22] baseline: 17
           "placerat"
-      frag 17 from TextNode start: 80, length: 1, rect: [636,98 29.625x22] baseline: 17
+      frag 19 from TextNode start: 80, length: 1, rect: [636,98 29.625x22] baseline: 17
           " "
-      frag 18 from TextNode start: 81, length: 7, rect: [665.625,98 73.875x22] baseline: 17
-          "mauris,"
-      frag 19 from TextNode start: 88, length: 1, rect: [739.625,98 29.625x22] baseline: 17
+      frag 20 from TextNode start: 81, length: 6, rect: [665.625,98 67.5625x22] baseline: 17
+          "mauris"
+      frag 21 from TextNode start: 87, length: 1, rect: [733.625,98 6.3125x22] baseline: 17
+          ","
+      frag 22 from TextNode start: 88, length: 1, rect: [739.625,98 29.625x22] baseline: 17
           " "
-      frag 20 from TextNode start: 89, length: 2, rect: [769.25,98 20.78125x22] baseline: 17
+      frag 23 from TextNode start: 89, length: 2, rect: [769.25,98 20.78125x22] baseline: 17
           "ut"
-      frag 21 from TextNode start: 92, length: 9, rect: [554,120 101.3125x22] baseline: 17
+      frag 24 from TextNode start: 92, length: 9, rect: [554,120 101.3125x22] baseline: 17
           "elementum"
-      frag 22 from TextNode start: 101, length: 1, rect: [655,120 10.421875x22] baseline: 17
+      frag 25 from TextNode start: 101, length: 1, rect: [655,120 10.421875x22] baseline: 17
           " "
-      frag 23 from TextNode start: 102, length: 3, rect: [665.421875,120 26.390625x22] baseline: 17
-          "mi."
-      frag 24 from TextNode start: 105, length: 1, rect: [692.421875,120 10.421875x22] baseline: 17
+      frag 26 from TextNode start: 102, length: 2, rect: [665.421875,120 20.953125x22] baseline: 17
+          "mi"
+      frag 27 from TextNode start: 104, length: 1, rect: [686.421875,120 5.4375x22] baseline: 17
+          "."
+      frag 28 from TextNode start: 105, length: 1, rect: [692.421875,120 10.421875x22] baseline: 17
           " "
-      frag 25 from TextNode start: 106, length: 5, rect: [702.84375,120 56.234375x22] baseline: 17
+      frag 29 from TextNode start: 106, length: 5, rect: [702.84375,120 56.234375x22] baseline: 17
           "Morbi"
-      frag 26 from TextNode start: 111, length: 1, rect: [758.84375,120 10.421875x22] baseline: 17
+      frag 30 from TextNode start: 111, length: 1, rect: [758.84375,120 10.421875x22] baseline: 17
           " "
-      frag 27 from TextNode start: 112, length: 2, rect: [769.265625,120 20.78125x22] baseline: 17
+      frag 31 from TextNode start: 112, length: 2, rect: [769.265625,120 20.78125x22] baseline: 17
           "ut"
-      frag 28 from TextNode start: 115, length: 8, rect: [554,142 78.78125x22] baseline: 17
+      frag 32 from TextNode start: 115, length: 8, rect: [554,142 78.78125x22] baseline: 17
           "vehicula"
-      frag 29 from TextNode start: 123, length: 1, rect: [633,142 27.21875x22] baseline: 17
+      frag 33 from TextNode start: 123, length: 1, rect: [633,142 27.21875x22] baseline: 17
           " "
-      frag 30 from TextNode start: 124, length: 6, rect: [660.21875,142 62.9375x22] baseline: 17
-          "ipsum,"
-      frag 31 from TextNode start: 130, length: 1, rect: [723.21875,142 27.21875x22] baseline: 17
+      frag 34 from TextNode start: 124, length: 5, rect: [660.21875,142 56.625x22] baseline: 17
+          "ipsum"
+      frag 35 from TextNode start: 129, length: 1, rect: [716.21875,142 6.3125x22] baseline: 17
+          ","
+      frag 36 from TextNode start: 130, length: 1, rect: [723.21875,142 27.21875x22] baseline: 17
           " "
-      frag 32 from TextNode start: 131, length: 4, rect: [750.4375,142 39.84375x22] baseline: 17
+      frag 37 from TextNode start: 131, length: 4, rect: [750.4375,142 39.84375x22] baseline: 17
           "eget"
-      frag 33 from TextNode start: 136, length: 8, rect: [554,164 82.078125x22] baseline: 17
+      frag 38 from TextNode start: 136, length: 8, rect: [554,164 82.078125x22] baseline: 17
           "placerat"
-      frag 34 from TextNode start: 144, length: 1, rect: [636,164 11.6875x22] baseline: 17
+      frag 39 from TextNode start: 144, length: 1, rect: [636,164 11.6875x22] baseline: 17
           " "
-      frag 35 from TextNode start: 145, length: 6, rect: [647.6875,164 61.890625x22] baseline: 17
-          "augue."
-      frag 36 from TextNode start: 151, length: 1, rect: [709.6875,164 11.6875x22] baseline: 17
+      frag 40 from TextNode start: 145, length: 5, rect: [647.6875,164 56.453125x22] baseline: 17
+          "augue"
+      frag 41 from TextNode start: 150, length: 1, rect: [704.6875,164 5.4375x22] baseline: 17
+          "."
+      frag 42 from TextNode start: 151, length: 1, rect: [709.6875,164 11.6875x22] baseline: 17
           " "
-      frag 37 from TextNode start: 152, length: 7, rect: [721.375,164 68.640625x22] baseline: 17
+      frag 43 from TextNode start: 152, length: 7, rect: [721.375,164 68.640625x22] baseline: 17
           "Integer"
-      frag 38 from TextNode start: 160, length: 6, rect: [554,186 70.296875x22] baseline: 17
+      frag 44 from TextNode start: 160, length: 6, rect: [554,186 70.296875x22] baseline: 17
           "rutrum"
-      frag 39 from TextNode start: 166, length: 1, rect: [624,186 21x22] baseline: 17
+      frag 45 from TextNode start: 166, length: 1, rect: [624,186 21x22] baseline: 17
           " "
-      frag 40 from TextNode start: 167, length: 4, rect: [645,186 35.109375x22] baseline: 17
+      frag 46 from TextNode start: 167, length: 4, rect: [645,186 35.109375x22] baseline: 17
           "nisi"
-      frag 41 from TextNode start: 171, length: 1, rect: [680,186 21x22] baseline: 17
+      frag 47 from TextNode start: 171, length: 1, rect: [680,186 21x22] baseline: 17
           " "
-      frag 42 from TextNode start: 172, length: 4, rect: [701,186 39.84375x22] baseline: 17
+      frag 48 from TextNode start: 172, length: 4, rect: [701,186 39.84375x22] baseline: 17
           "eget"
-      frag 43 from TextNode start: 176, length: 1, rect: [741,186 21x22] baseline: 17
+      frag 49 from TextNode start: 176, length: 1, rect: [741,186 21x22] baseline: 17
           " "
-      frag 44 from TextNode start: 177, length: 3, rect: [762,186 27.734375x22] baseline: 17
+      frag 50 from TextNode start: 177, length: 3, rect: [762,186 27.734375x22] baseline: 17
           "dui"
-      frag 45 from TextNode start: 181, length: 7, rect: [252,212 68.984375x22] baseline: 17
-          "dictum,"
-      frag 46 from TextNode start: 188, length: 1, rect: [321,212 23.578125x22] baseline: 17
+      frag 51 from TextNode start: 181, length: 6, rect: [252,212 62.671875x22] baseline: 17
+          "dictum"
+      frag 52 from TextNode start: 187, length: 1, rect: [315,212 6.3125x22] baseline: 17
+          ","
+      frag 53 from TextNode start: 188, length: 1, rect: [321,212 23.578125x22] baseline: 17
           " "
-      frag 47 from TextNode start: 189, length: 2, rect: [344.578125,212 23.109375x22] baseline: 17
+      frag 54 from TextNode start: 189, length: 2, rect: [344.578125,212 23.109375x22] baseline: 17
           "eu"
-      frag 48 from TextNode start: 191, length: 1, rect: [367.578125,212 23.578125x22] baseline: 17
+      frag 55 from TextNode start: 191, length: 1, rect: [367.578125,212 23.578125x22] baseline: 17
           " "
-      frag 49 from TextNode start: 192, length: 8, rect: [391.15625,212 96.75x22] baseline: 17
+      frag 56 from TextNode start: 192, length: 8, rect: [391.15625,212 96.75x22] baseline: 17
           "accumsan"
-      frag 50 from TextNode start: 201, length: 4, rect: [252,234 43.875x22] baseline: 17
+      frag 57 from TextNode start: 201, length: 4, rect: [252,234 43.875x22] baseline: 17
           "enim"
-      frag 51 from TextNode start: 205, length: 1, rect: [296,234 37.875x22] baseline: 17
+      frag 58 from TextNode start: 205, length: 1, rect: [296,234 37.875x22] baseline: 17
           " "
-      frag 52 from TextNode start: 206, length: 10, rect: [333.875,234 93.65625x22] baseline: 17
-          "tristique."
-      frag 53 from TextNode start: 216, length: 1, rect: [427.875,234 37.875x22] baseline: 17
+      frag 59 from TextNode start: 206, length: 9, rect: [333.875,234 88.21875x22] baseline: 17
+          "tristique"
+      frag 60 from TextNode start: 215, length: 1, rect: [421.875,234 5.4375x22] baseline: 17
+          "."
+      frag 61 from TextNode start: 216, length: 1, rect: [427.875,234 37.875x22] baseline: 17
           " "
-      frag 54 from TextNode start: 217, length: 2, rect: [465.75,234 22.703125x22] baseline: 17
+      frag 62 from TextNode start: 217, length: 2, rect: [465.75,234 22.703125x22] baseline: 17
           "Ut"
-      frag 55 from TextNode start: 220, length: 8, rect: [252,256 80.046875x22] baseline: 17
+      frag 63 from TextNode start: 220, length: 8, rect: [252,256 80.046875x22] baseline: 17
           "lobortis"
-      frag 56 from TextNode start: 228, length: 1, rect: [332,256 30.328125x22] baseline: 17
+      frag 64 from TextNode start: 228, length: 1, rect: [332,256 30.328125x22] baseline: 17
           " "
-      frag 57 from TextNode start: 229, length: 5, rect: [362.328125,256 55.4375x22] baseline: 17
+      frag 65 from TextNode start: 229, length: 5, rect: [362.328125,256 55.4375x22] baseline: 17
           "lorem"
-      frag 58 from TextNode start: 234, length: 1, rect: [417.328125,256 30.328125x22] baseline: 17
+      frag 66 from TextNode start: 234, length: 1, rect: [417.328125,256 30.328125x22] baseline: 17
           " "
-      frag 59 from TextNode start: 235, length: 4, rect: [447.65625,256 39.84375x22] baseline: 17
+      frag 67 from TextNode start: 235, length: 4, rect: [447.65625,256 39.84375x22] baseline: 17
           "eget"
-      frag 60 from TextNode start: 240, length: 3, rect: [252,278 31.171875x22] baseline: 17
+      frag 68 from TextNode start: 240, length: 3, rect: [252,278 31.171875x22] baseline: 17
           "est"
-      frag 61 from TextNode start: 243, length: 1, rect: [283,278 16.5x22] baseline: 17
+      frag 69 from TextNode start: 243, length: 1, rect: [283,278 16.5x22] baseline: 17
           " "
-      frag 62 from TextNode start: 244, length: 9, rect: [299.5,278 91.484375x22] baseline: 17
+      frag 70 from TextNode start: 244, length: 9, rect: [299.5,278 91.484375x22] baseline: 17
           "vulputate"
-      frag 63 from TextNode start: 253, length: 1, rect: [391.5,278 16.5x22] baseline: 17
+      frag 71 from TextNode start: 253, length: 1, rect: [391.5,278 16.5x22] baseline: 17
           " "
-      frag 64 from TextNode start: 254, length: 8, rect: [408,278 80.34375x22] baseline: 17
-          "egestas."
-      frag 65 from TextNode start: 263, length: 7, rect: [252,300 68.640625x22] baseline: 17
+      frag 72 from TextNode start: 254, length: 7, rect: [408,278 74.90625x22] baseline: 17
+          "egestas"
+      frag 73 from TextNode start: 261, length: 1, rect: [483,278 5.4375x22] baseline: 17
+          "."
+      frag 74 from TextNode start: 263, length: 7, rect: [252,300 68.640625x22] baseline: 17
           "Integer"
-      frag 66 from TextNode start: 270, length: 1, rect: [321,300 16.390625x22] baseline: 17
+      frag 75 from TextNode start: 270, length: 1, rect: [321,300 16.390625x22] baseline: 17
           " "
-      frag 67 from TextNode start: 271, length: 7, rect: [337.390625,300 71.359375x22] baseline: 17
+      frag 76 from TextNode start: 271, length: 7, rect: [337.390625,300 71.359375x22] baseline: 17
           "laoreet"
-      frag 68 from TextNode start: 278, length: 1, rect: [408.390625,300 16.390625x22] baseline: 17
+      frag 77 from TextNode start: 278, length: 1, rect: [408.390625,300 16.390625x22] baseline: 17
           " "
-      frag 69 from TextNode start: 279, length: 7, rect: [424.78125,300 63.203125x22] baseline: 17
+      frag 78 from TextNode start: 279, length: 7, rect: [424.78125,300 63.203125x22] baseline: 17
           "lacinia"
-      frag 70 from TextNode start: 287, length: 4, rect: [252,322 43.1875x22] baseline: 17
+      frag 79 from TextNode start: 287, length: 4, rect: [252,322 43.1875x22] baseline: 17
           "ante"
-      frag 71 from TextNode start: 291, length: 1, rect: [295,322 16.640625x22] baseline: 17
+      frag 80 from TextNode start: 291, length: 1, rect: [295,322 16.640625x22] baseline: 17
           " "
-      frag 72 from TextNode start: 292, length: 7, rect: [311.640625,322 74.046875x22] baseline: 17
+      frag 81 from TextNode start: 292, length: 7, rect: [311.640625,322 74.046875x22] baseline: 17
           "sodales"
-      frag 73 from TextNode start: 299, length: 1, rect: [385.640625,322 16.640625x22] baseline: 17
+      frag 82 from TextNode start: 299, length: 1, rect: [385.640625,322 16.640625x22] baseline: 17
           " "
-      frag 74 from TextNode start: 300, length: 9, rect: [402.28125,322 85.484375x22] baseline: 17
-          "lobortis."
-      frag 75 from TextNode start: 310, length: 5, rect: [252,344 60.90625x22] baseline: 17
+      frag 83 from TextNode start: 300, length: 8, rect: [402.28125,322 80.046875x22] baseline: 17
+          "lobortis"
+      frag 84 from TextNode start: 308, length: 1, rect: [482.28125,322 5.4375x22] baseline: 17
+          "."
+      frag 85 from TextNode start: 310, length: 5, rect: [252,344 60.90625x22] baseline: 17
           "Donec"
-      frag 76 from TextNode start: 315, length: 1, rect: [313,344 38.828125x22] baseline: 17
+      frag 86 from TextNode start: 315, length: 1, rect: [313,344 38.828125x22] baseline: 17
           " "
-      frag 77 from TextNode start: 316, length: 1, rect: [351.828125,344 11.6875x22] baseline: 17
+      frag 87 from TextNode start: 316, length: 1, rect: [351.828125,344 11.6875x22] baseline: 17
           "a"
-      frag 78 from TextNode start: 317, length: 1, rect: [363.828125,344 38.828125x22] baseline: 17
+      frag 88 from TextNode start: 317, length: 1, rect: [363.828125,344 38.828125x22] baseline: 17
           " "
-      frag 79 from TextNode start: 318, length: 9, rect: [402.65625,344 85.734375x22] baseline: 17
+      frag 89 from TextNode start: 318, length: 9, rect: [402.65625,344 85.734375x22] baseline: 17
           "tincidunt"
-      frag 80 from TextNode start: 328, length: 5, rect: [252,366 48.625x22] baseline: 17
-          "ante."
-      frag 81 from TextNode start: 333, length: 1, rect: [301,366 11.609375x22] baseline: 17
+      frag 90 from TextNode start: 328, length: 4, rect: [252,366 43.1875x22] baseline: 17
+          "ante"
+      frag 91 from TextNode start: 332, length: 1, rect: [295,366 5.4375x22] baseline: 17
+          "."
+      frag 92 from TextNode start: 333, length: 1, rect: [301,366 11.609375x22] baseline: 17
           " "
-      frag 82 from TextNode start: 334, length: 9, rect: [312.609375,366 94.8125x22] baseline: 17
+      frag 93 from TextNode start: 334, length: 9, rect: [312.609375,366 94.8125x22] baseline: 17
           "Phasellus"
-      frag 83 from TextNode start: 343, length: 1, rect: [406.609375,366 11.609375x22] baseline: 17
+      frag 94 from TextNode start: 343, length: 1, rect: [406.609375,366 11.609375x22] baseline: 17
           " "
-      frag 84 from TextNode start: 344, length: 1, rect: [418.21875,366 11.6875x22] baseline: 17
+      frag 95 from TextNode start: 344, length: 1, rect: [418.21875,366 11.6875x22] baseline: 17
           "a"
-      frag 85 from TextNode start: 345, length: 1, rect: [430.21875,366 11.609375x22] baseline: 17
+      frag 96 from TextNode start: 345, length: 1, rect: [430.21875,366 11.609375x22] baseline: 17
           " "
-      frag 86 from TextNode start: 346, length: 4, rect: [441.828125,366 46.03125x22] baseline: 17
+      frag 97 from TextNode start: 346, length: 4, rect: [441.828125,366 46.03125x22] baseline: 17
           "arcu"
-      frag 87 from TextNode start: 351, length: 7, rect: [252,388 70.5625x22] baseline: 17
-          "tortor."
+      frag 98 from TextNode start: 351, length: 6, rect: [252,388 65.125x22] baseline: 17
+          "tortor"
+      frag 99 from TextNode start: 357, length: 1, rect: [317,388 5.4375x22] baseline: 17
+          "."
       BlockContainer <div.left> at (253,11) content-size 300x200 floating [BFC] children: not-inline
       TextNode <#text>
       BlockContainer <div.right> at (489,213) content-size 300x200 floating [BFC] children: not-inline
@@ -184,7 +208,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
 
 ViewportPaintable (Viewport<#document>) [0,0 800x600] overflow: [0,0 800x602]
   PaintableWithLines (BlockContainer<HTML>) [0,0 800x602]
-    PaintableWithLines (BlockContainer<BODY>) [251,9 540x402] overflow: [252,10 538.328125x400]
+    PaintableWithLines (BlockContainer<BODY>) [251,9 540x402] overflow: [252,10 538.28125x400]
       PaintableWithLines (BlockContainer<DIV>.left) [252,10 302x202]
       PaintableWithLines (BlockContainer<DIV>.right) [488,212 302x202]
       TextPaintable (TextNode<#text>)

+ 27 - 17
Tests/LibWeb/Layout/expected/div_align.txt

@@ -39,36 +39,46 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
               "is"
           frag 5 from TextNode start: 12, length: 1, rect: [106,479 8x17] baseline: 13.296875
               " "
-          frag 6 from TextNode start: 13, length: 16, rect: [114,479 102.96875x17] baseline: 13.296875
-              "'full-justified'"
-          frag 7 from TextNode start: 29, length: 1, rect: [217,479 8x17] baseline: 13.296875
+          frag 6 from TextNode start: 13, length: 1, rect: [114,479 3.625x17] baseline: 13.296875
+              "'"
+          frag 7 from TextNode start: 14, length: 4, rect: [117,479 24.671875x17] baseline: 13.296875
+              "full"
+          frag 8 from TextNode start: 18, length: 1, rect: [142,479 6.484375x17] baseline: 13.296875
+              "-"
+          frag 9 from TextNode start: 19, length: 9, rect: [148,479 64.5625x17] baseline: 13.296875
+              "justified"
+          frag 10 from TextNode start: 28, length: 1, rect: [213,479 3.625x17] baseline: 13.296875
+              "'"
+          frag 11 from TextNode start: 29, length: 1, rect: [217,479 8x17] baseline: 13.296875
               " "
-          frag 8 from TextNode start: 30, length: 3, rect: [225,479 26.8125x17] baseline: 13.296875
+          frag 12 from TextNode start: 30, length: 3, rect: [225,479 26.8125x17] baseline: 13.296875
               "and"
-          frag 9 from TextNode start: 33, length: 1, rect: [251,479 8x17] baseline: 13.296875
+          frag 13 from TextNode start: 33, length: 1, rect: [251,479 8x17] baseline: 13.296875
               " "
-          frag 10 from TextNode start: 34, length: 3, rect: [259,479 24.875x17] baseline: 13.296875
+          frag 14 from TextNode start: 34, length: 3, rect: [259,479 24.875x17] baseline: 13.296875
               "the"
-          frag 11 from TextNode start: 37, length: 1, rect: [284,479 8x17] baseline: 13.296875
+          frag 15 from TextNode start: 37, length: 1, rect: [284,479 8x17] baseline: 13.296875
               " "
-          frag 12 from TextNode start: 38, length: 5, rect: [292,479 43.4375x17] baseline: 13.296875
+          frag 16 from TextNode start: 38, length: 5, rect: [292,479 43.4375x17] baseline: 13.296875
               "green"
-          frag 13 from TextNode start: 43, length: 1, rect: [336,479 8x17] baseline: 13.296875
+          frag 17 from TextNode start: 43, length: 1, rect: [336,479 8x17] baseline: 13.296875
               " "
-          frag 14 from TextNode start: 44, length: 6, rect: [344,479 57.0625x17] baseline: 13.296875
+          frag 18 from TextNode start: 44, length: 6, rect: [344,479 57.0625x17] baseline: 13.296875
               "square"
-          frag 15 from TextNode start: 50, length: 1, rect: [401,479 8x17] baseline: 13.296875
+          frag 19 from TextNode start: 50, length: 1, rect: [401,479 8x17] baseline: 13.296875
               " "
-          frag 16 from TextNode start: 51, length: 2, rect: [409,479 13.90625x17] baseline: 13.296875
+          frag 20 from TextNode start: 51, length: 2, rect: [409,479 13.90625x17] baseline: 13.296875
               "is"
-          frag 17 from TextNode start: 53, length: 1, rect: [423,479 8x17] baseline: 13.296875
+          frag 21 from TextNode start: 53, length: 1, rect: [423,479 8x17] baseline: 13.296875
               " "
-          frag 18 from TextNode start: 54, length: 4, rect: [431,479 26.25x17] baseline: 13.296875
+          frag 22 from TextNode start: 54, length: 4, rect: [431,479 26.25x17] baseline: 13.296875
               "left"
-          frag 19 from TextNode start: 58, length: 1, rect: [457,479 8x17] baseline: 13.296875
+          frag 23 from TextNode start: 58, length: 1, rect: [457,479 8x17] baseline: 13.296875
               " "
-          frag 20 from TextNode start: 59, length: 8, rect: [465,479 55.671875x17] baseline: 13.296875
-              "aligned:"
+          frag 24 from TextNode start: 59, length: 7, rect: [465,479 51.890625x17] baseline: 13.296875
+              "aligned"
+          frag 25 from TextNode start: 66, length: 1, rect: [517,479 3.78125x17] baseline: 13.296875
+              ":"
           TextNode <#text>
         BlockContainer <div.square> at (28,516) content-size 100x100 children: not-inline
       BlockContainer <(anonymous)> at (8,636) content-size 784x0 children: inline

BIN
Tests/LibWeb/Screenshot/images/text-direction-ref.png


+ 10 - 0
Tests/LibWeb/Screenshot/reference/text-direction-ref.html

@@ -0,0 +1,10 @@
+<style>
+    * {
+        margin: 0;
+    }
+
+    body {
+        background-color: white;
+    }
+</style>
+<img src="../images/text-direction-ref.png">

+ 18 - 0
Tests/LibWeb/Screenshot/text-direction.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<link rel="match" href="reference/text-direction-ref.html"/>
+<div dir="ltr" align="left">hello test 1, 2, 3!</div>
+<div dir="rtl" align="left">hello test 1, 2, 3!</div>
+<div dir="ltr" align="right">hello test 1, 2, 3!</div>
+<div dir="rtl" align="right">hello test 1, 2, 3!</div>
+
+<div dir=ltr>אא aaa bbb ccc מממ</div>
+<div dir=rtl>אא aaa bbb ccc מממ</div>
+
+<div dir=ltr>אא 1 2 3 מממ</div>
+<div dir=rtl>אא 1 2 3 מממ</div>
+
+<div dir=ltr>aa....!!!</div>
+<div dir=rtl>aa....!!!</div>
+
+<div dir=ltr>حسنًا ، hello friends مرحباً أيها ا test لأصدقاء end</div>
+<div dir=rtl>حسنًا ، hello friends مرحباً أيها ا test لأصدقاء end</div>

+ 12 - 1
Userland/Libraries/LibGfx/TextLayout.h

@@ -44,13 +44,23 @@ using DrawGlyphOrEmoji = Variant<DrawGlyph, DrawEmoji>;
 
 class GlyphRun : public RefCounted<GlyphRun> {
 public:
-    GlyphRun(Vector<Gfx::DrawGlyphOrEmoji>&& glyphs, NonnullRefPtr<Font> font)
+    enum class TextType {
+        Common,
+        ContextDependent,
+        EndPadding,
+        Ltr,
+        Rtl,
+    };
+
+    GlyphRun(Vector<Gfx::DrawGlyphOrEmoji>&& glyphs, NonnullRefPtr<Font> font, TextType text_type)
         : m_glyphs(move(glyphs))
         , m_font(move(font))
+        , m_text_type(text_type)
     {
     }
 
     [[nodiscard]] Font const& font() const { return m_font; }
+    [[nodiscard]] TextType text_type() const { return m_text_type; }
     [[nodiscard]] Vector<Gfx::DrawGlyphOrEmoji> const& glyphs() const { return m_glyphs; }
     [[nodiscard]] Vector<Gfx::DrawGlyphOrEmoji>& glyphs() { return m_glyphs; }
     [[nodiscard]] bool is_empty() const { return m_glyphs.is_empty(); }
@@ -60,6 +70,7 @@ public:
 private:
     Vector<Gfx::DrawGlyphOrEmoji> m_glyphs;
     NonnullRefPtr<Font> m_font;
+    TextType m_text_type;
 };
 
 void for_each_glyph_position(FloatPoint baseline_start, Utf8View string, Gfx::Font const& font, Function<void(DrawGlyphOrEmoji const&)> callback, Optional<float&> width = {});

+ 3 - 1
Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp

@@ -249,8 +249,10 @@ void InlineFormattingContext::generate_line_boxes(LayoutMode layout_mode)
     auto& line_boxes = m_containing_block_used_values.line_boxes;
     line_boxes.clear_with_capacity();
 
+    auto direction = m_context_box->computed_values().direction();
+
     InlineLevelIterator iterator(*this, m_state, containing_block(), m_containing_block_used_values, layout_mode);
-    LineBuilder line_builder(*this, m_state, m_containing_block_used_values);
+    LineBuilder line_builder(*this, m_state, m_containing_block_used_values, direction);
 
     // NOTE: When we ignore collapsible whitespace chunks at the start of a line,
     //       we have to remember how much start margin that chunk had in the inline

+ 48 - 5
Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp

@@ -165,6 +165,39 @@ CSSPixels InlineLevelIterator::next_non_whitespace_sequence_width()
     return next_width;
 }
 
+Gfx::GlyphRun::TextType InlineLevelIterator::resolve_text_direction_from_context()
+{
+    VERIFY(m_text_node_context.has_value());
+
+    Optional<Gfx::GlyphRun::TextType> next_known_direction;
+    for (size_t i = 0;; ++i) {
+        auto peek = m_text_node_context->chunk_iterator.peek(i);
+        if (!peek.has_value())
+            break;
+        if (peek->text_type == Gfx::GlyphRun::TextType::Ltr || peek->text_type == Gfx::GlyphRun::TextType::Rtl) {
+            next_known_direction = peek->text_type;
+            break;
+        }
+    }
+
+    auto last_known_direction = m_text_node_context->last_known_direction;
+    if (last_known_direction.has_value() && next_known_direction.has_value() && *last_known_direction != *next_known_direction) {
+        switch (m_containing_block->computed_values().direction()) {
+        case CSS::Direction::Ltr:
+            return Gfx::GlyphRun::TextType::Ltr;
+        case CSS::Direction::Rtl:
+            return Gfx::GlyphRun::TextType::Rtl;
+        }
+    }
+
+    if (last_known_direction.has_value())
+        return *last_known_direction;
+    if (next_known_direction.has_value())
+        return *next_known_direction;
+
+    return Gfx::GlyphRun::TextType::ContextDependent;
+}
+
 Optional<InlineLevelIterator::Item> InlineLevelIterator::next_without_lookahead()
 {
     if (!m_current_node)
@@ -176,18 +209,29 @@ Optional<InlineLevelIterator::Item> InlineLevelIterator::next_without_lookahead(
         if (!m_text_node_context.has_value())
             enter_text_node(text_node);
 
-        auto chunk_opt = m_text_node_context->next_chunk;
+        auto chunk_opt = m_text_node_context->chunk_iterator.next();
         if (!chunk_opt.has_value()) {
             m_text_node_context = {};
             skip_to_next();
             return next_without_lookahead();
         }
 
-        m_text_node_context->next_chunk = m_text_node_context->chunk_iterator.next();
-        if (!m_text_node_context->next_chunk.has_value())
+        if (!m_text_node_context->chunk_iterator.peek(0).has_value())
             m_text_node_context->is_last_chunk = true;
 
         auto& chunk = chunk_opt.value();
+        auto text_type = chunk.text_type;
+        if (text_type == Gfx::GlyphRun::TextType::Ltr || text_type == Gfx::GlyphRun::TextType::Rtl)
+            m_text_node_context->last_known_direction = text_type;
+
+        if (m_text_node_context->do_respect_linebreaks && chunk.has_breaking_newline) {
+            m_text_node_context->is_last_chunk = true;
+            if (chunk.is_all_whitespace)
+                text_type = Gfx::GlyphRun::TextType::EndPadding;
+        }
+
+        if (text_type == Gfx::GlyphRun::TextType::ContextDependent)
+            text_type = resolve_text_direction_from_context();
 
         if (m_text_node_context->do_respect_linebreaks && chunk.has_breaking_newline) {
             return Item {
@@ -211,7 +255,7 @@ Optional<InlineLevelIterator::Item> InlineLevelIterator::next_without_lookahead(
         Item item {
             .type = Item::Type::Text,
             .node = &text_node,
-            .glyph_run = adopt_ref(*new Gfx::GlyphRun(move(glyph_run), chunk.font)),
+            .glyph_run = adopt_ref(*new Gfx::GlyphRun(move(glyph_run), chunk.font, text_type)),
             .offset_in_node = chunk.start,
             .length_in_node = chunk.length,
             .width = chunk_width,
@@ -322,7 +366,6 @@ void InlineLevelIterator::enter_text_node(Layout::TextNode const& text_node)
         .is_last_chunk = false,
         .chunk_iterator = TextNode::ChunkIterator { text_node.text_for_rendering(), do_wrap_lines, do_respect_linebreaks, text_node.computed_values().font_list() },
     };
-    m_text_node_context->next_chunk = m_text_node_context->chunk_iterator.next();
 }
 
 void InlineLevelIterator::add_extra_box_model_metrics_to_item(Item& item, bool add_leading_metrics, bool add_trailing_metrics)

+ 2 - 1
Userland/Libraries/LibWeb/Layout/InlineLevelIterator.h

@@ -57,6 +57,7 @@ public:
 
 private:
     Optional<Item> next_without_lookahead();
+    Gfx::GlyphRun::TextType resolve_text_direction_from_context();
     void skip_to_next();
     void compute_next();
 
@@ -84,7 +85,7 @@ private:
         bool is_first_chunk {};
         bool is_last_chunk {};
         TextNode::ChunkIterator chunk_iterator;
-        Optional<TextNode::Chunk> next_chunk {};
+        Optional<Gfx::GlyphRun::TextType> last_known_direction {};
     };
 
     Optional<TextNodeContext> m_text_node_context;

+ 2 - 7
Userland/Libraries/LibWeb/Layout/LineBox.cpp

@@ -19,19 +19,14 @@ void LineBox::add_fragment(Node const& layout_node, int start, int length, CSSPi
 {
     bool text_align_is_justify = layout_node.computed_values().text_align() == CSS::TextAlign::Justify;
     if (glyph_run && !text_align_is_justify && !m_fragments.is_empty() && &m_fragments.last().layout_node() == &layout_node && &m_fragments.last().m_glyph_run->font() == &glyph_run->font()) {
-        auto const fragment_width = m_fragments.last().width();
         // The fragment we're adding is from the last Layout::Node on the line.
         // Expand the last fragment instead of adding a new one with the same Layout::Node.
         m_fragments.last().m_length = (start - m_fragments.last().m_start) + length;
-        m_fragments.last().set_width(m_fragments.last().width() + content_width);
-        for (auto& glyph : glyph_run->glyphs()) {
-            glyph.visit([&](auto& glyph) { glyph.position.translate_by(fragment_width.to_float(), 0); });
-            m_fragments.last().m_glyph_run->append(glyph);
-        }
+        m_fragments.last().append_glyph_run(glyph_run, content_width);
     } else {
         CSSPixels x_offset = leading_margin + leading_size + m_width;
         CSSPixels y_offset = 0;
-        m_fragments.append(LineBoxFragment { layout_node, start, length, CSSPixelPoint(x_offset, y_offset), CSSPixelSize(content_width, content_height), border_box_top, move(glyph_run) });
+        m_fragments.append(LineBoxFragment { layout_node, start, length, CSSPixelPoint(x_offset, y_offset), CSSPixelSize(content_width, content_height), border_box_top, m_direction, move(glyph_run) });
     }
     m_width += leading_margin + leading_size + content_width + trailing_size + trailing_margin;
     m_height = max(m_height, content_height + border_box_top + border_box_bottom);

+ 5 - 1
Userland/Libraries/LibWeb/Layout/LineBox.h

@@ -13,7 +13,10 @@ namespace Web::Layout {
 
 class LineBox {
 public:
-    LineBox() = default;
+    LineBox(CSS::Direction direction)
+        : m_direction(direction)
+    {
+    }
 
     CSSPixels width() const { return m_width; }
     CSSPixels height() const { return m_height; }
@@ -42,6 +45,7 @@ private:
     CSSPixels m_height { 0 };
     CSSPixels m_bottom { 0 };
     CSSPixels m_baseline { 0 };
+    CSS::Direction m_direction { CSS::Direction::Ltr };
 
     // The amount of available width that was originally available when creating this line box. Used for text justification.
     AvailableSize m_original_available_width { AvailableSize::make_indefinite() };

+ 118 - 0
Userland/Libraries/LibWeb/Layout/LineBoxFragment.cpp

@@ -12,6 +12,23 @@
 
 namespace Web::Layout {
 
+LineBoxFragment::LineBoxFragment(Node const& layout_node, int start, int length, CSSPixelPoint offset, CSSPixelSize size, CSSPixels border_box_top, CSS::Direction direction, RefPtr<Gfx::GlyphRun> glyph_run)
+    : m_layout_node(layout_node)
+    , m_start(start)
+    , m_length(length)
+    , m_offset(offset)
+    , m_size(size)
+    , m_border_box_top(border_box_top)
+    , m_direction(direction)
+    , m_glyph_run(move(glyph_run))
+{
+    if (m_glyph_run) {
+        m_current_insert_direction = resolve_glyph_run_direction(m_glyph_run->text_type());
+        if (m_direction == CSS::Direction::Rtl)
+            m_insert_position = m_size.width().to_float();
+    }
+}
+
 bool LineBoxFragment::ends_in_whitespace() const
 {
     auto text = this->text();
@@ -45,4 +62,105 @@ bool LineBoxFragment::is_atomic_inline() const
     return layout_node().is_replaced_box() || (layout_node().display().is_inline_outside() && !layout_node().display().is_flow_inside());
 }
 
+CSS::Direction LineBoxFragment::resolve_glyph_run_direction(Gfx::GlyphRun::TextType text_type) const
+{
+    switch (text_type) {
+    case Gfx::GlyphRun::TextType::Common:
+    case Gfx::GlyphRun::TextType::ContextDependent:
+    case Gfx::GlyphRun::TextType::EndPadding:
+        return m_direction;
+    case Gfx::GlyphRun::TextType::Ltr:
+        return CSS::Direction::Ltr;
+    case Gfx::GlyphRun::TextType::Rtl:
+        return CSS::Direction::Rtl;
+    default:
+        VERIFY_NOT_REACHED();
+    }
+}
+
+void LineBoxFragment::append_glyph_run(RefPtr<Gfx::GlyphRun> const& glyph_run, CSSPixels run_width)
+{
+    switch (m_direction) {
+    case CSS::Direction::Ltr:
+        append_glyph_run_ltr(glyph_run, run_width);
+        break;
+    case CSS::Direction::Rtl:
+        append_glyph_run_rtl(glyph_run, run_width);
+        break;
+    }
+}
+
+void LineBoxFragment::append_glyph_run_ltr(RefPtr<Gfx::GlyphRun> const& glyph_run, CSSPixels run_width)
+{
+    auto run_direction = resolve_glyph_run_direction(glyph_run->text_type());
+
+    if (m_current_insert_direction != run_direction) {
+        if (run_direction == CSS::Direction::Rtl)
+            m_insert_position = width().to_float();
+        m_current_insert_direction = run_direction;
+    }
+
+    switch (run_direction) {
+    case CSS::Direction::Ltr:
+        for (auto& glyph : glyph_run->glyphs()) {
+            glyph.visit([&](auto& glyph) { glyph.position.translate_by(width().to_float(), 0); });
+            m_glyph_run->append(glyph);
+        }
+        break;
+    case CSS::Direction::Rtl:
+        for (auto& glyph : m_glyph_run->glyphs()) {
+            glyph.visit([&](auto& glyph) {
+                if (glyph.position.x() >= m_insert_position)
+                    glyph.position.translate_by(run_width.to_float(), 0);
+            });
+        }
+        for (auto& glyph : glyph_run->glyphs()) {
+            glyph.visit([&](auto& glyph) { glyph.position.translate_by(m_insert_position, 0); });
+            m_glyph_run->append(glyph);
+        }
+        break;
+    }
+
+    m_size.set_width(width() + run_width);
+}
+
+void LineBoxFragment::append_glyph_run_rtl(RefPtr<Gfx::GlyphRun> const& glyph_run, CSSPixels run_width)
+{
+    auto run_direction = resolve_glyph_run_direction(glyph_run->text_type());
+
+    if (m_current_insert_direction != run_direction) {
+        if (run_direction == CSS::Direction::Ltr)
+            m_insert_position = 0;
+        m_current_insert_direction = run_direction;
+    }
+
+    switch (run_direction) {
+    case CSS::Direction::Ltr:
+        for (auto& glyph : m_glyph_run->glyphs()) {
+            glyph.visit([&](auto& glyph) {
+                if (glyph.position.x() >= m_insert_position)
+                    glyph.position.translate_by(run_width.to_float(), 0);
+            });
+        }
+        for (auto& glyph : glyph_run->glyphs()) {
+            glyph.visit([&](auto& glyph) { glyph.position.translate_by(m_insert_position, 0); });
+            m_glyph_run->append(glyph);
+        }
+        break;
+    case CSS::Direction::Rtl:
+        if (glyph_run->text_type() != Gfx::GlyphRun::TextType::EndPadding) {
+            for (auto& glyph : m_glyph_run->glyphs()) {
+                glyph.visit([&](auto& glyph) { glyph.position.translate_by(run_width.to_float(), 0); });
+            }
+        }
+        for (auto& glyph : glyph_run->glyphs()) {
+            m_glyph_run->append(glyph);
+        }
+        break;
+    }
+
+    m_size.set_width(width() + run_width);
+    m_insert_position += run_width.to_float();
+}
+
 }

+ 10 - 10
Userland/Libraries/LibWeb/Layout/LineBoxFragment.h

@@ -19,16 +19,7 @@ class LineBoxFragment {
     friend class LineBox;
 
 public:
-    LineBoxFragment(Node const& layout_node, int start, int length, CSSPixelPoint offset, CSSPixelSize size, CSSPixels border_box_top, RefPtr<Gfx::GlyphRun> glyph_run)
-        : m_layout_node(layout_node)
-        , m_start(start)
-        , m_length(length)
-        , m_offset(offset)
-        , m_size(size)
-        , m_border_box_top(border_box_top)
-        , m_glyph_run(move(glyph_run))
-    {
-    }
+    LineBoxFragment(Node const& layout_node, int start, int length, CSSPixelPoint offset, CSSPixelSize size, CSSPixels border_box_top, CSS::Direction, RefPtr<Gfx::GlyphRun>);
 
     Node const& layout_node() const { return m_layout_node; }
     int start() const { return m_start; }
@@ -60,8 +51,13 @@ public:
     bool is_atomic_inline() const;
 
     RefPtr<Gfx::GlyphRun> glyph_run() const { return m_glyph_run; }
+    void append_glyph_run(RefPtr<Gfx::GlyphRun> const&, CSSPixels run_width);
 
 private:
+    CSS::Direction resolve_glyph_run_direction(Gfx::GlyphRun::TextType) const;
+    void append_glyph_run_ltr(RefPtr<Gfx::GlyphRun> const&, CSSPixels run_width);
+    void append_glyph_run_rtl(RefPtr<Gfx::GlyphRun> const&, CSSPixels run_width);
+
     JS::NonnullGCPtr<Node const> m_layout_node;
     int m_start { 0 };
     int m_length { 0 };
@@ -69,7 +65,11 @@ private:
     CSSPixelSize m_size;
     CSSPixels m_border_box_top { 0 };
     CSSPixels m_baseline { 0 };
+    CSS::Direction m_direction { CSS::Direction::Ltr };
+
     RefPtr<Gfx::GlyphRun> m_glyph_run;
+    float m_insert_position { 0 };
+    CSS::Direction m_current_insert_direction { CSS::Direction::Ltr };
 };
 
 }

+ 4 - 3
Userland/Libraries/LibWeb/Layout/LineBuilder.cpp

@@ -10,10 +10,11 @@
 
 namespace Web::Layout {
 
-LineBuilder::LineBuilder(InlineFormattingContext& context, LayoutState& layout_state, LayoutState::UsedValues& containing_block_used_values)
+LineBuilder::LineBuilder(InlineFormattingContext& context, LayoutState& layout_state, LayoutState::UsedValues& containing_block_used_values, CSS::Direction direction)
     : m_context(context)
     , m_layout_state(layout_state)
     , m_containing_block_used_values(containing_block_used_values)
+    , m_direction(direction)
 {
     m_text_indent = m_context.containing_block().computed_values().text_indent().to_px(m_context.containing_block(), m_containing_block_used_values.content_width());
     begin_new_line(false);
@@ -35,7 +36,7 @@ void LineBuilder::break_line(ForcedBreak forced_break, Optional<CSSPixels> next_
     size_t break_count = 0;
     bool floats_intrude_at_current_y = false;
     do {
-        m_containing_block_used_values.line_boxes.append(LineBox());
+        m_containing_block_used_values.line_boxes.append(LineBox(m_direction));
         begin_new_line(true, break_count == 0);
         break_count++;
         floats_intrude_at_current_y = m_context.any_floats_intrude_at_y(m_current_y);
@@ -80,7 +81,7 @@ LineBox& LineBuilder::ensure_last_line_box()
 {
     auto& line_boxes = m_containing_block_used_values.line_boxes;
     if (line_boxes.is_empty())
-        line_boxes.append(LineBox {});
+        line_boxes.append(LineBox(m_direction));
     return line_boxes.last();
 }
 

+ 2 - 1
Userland/Libraries/LibWeb/Layout/LineBuilder.h

@@ -15,7 +15,7 @@ class LineBuilder {
     AK_MAKE_NONMOVABLE(LineBuilder);
 
 public:
-    LineBuilder(InlineFormattingContext&, LayoutState&, LayoutState::UsedValues& containing_block_used_values);
+    LineBuilder(InlineFormattingContext&, LayoutState&, LayoutState::UsedValues& containing_block_used_values, CSS::Direction);
     ~LineBuilder();
 
     enum class ForcedBreak {
@@ -63,6 +63,7 @@ private:
     CSSPixels m_current_y { 0 };
     CSSPixels m_max_height_on_current_line { 0 };
     CSSPixels m_text_indent { 0 };
+    CSS::Direction m_direction { CSS::Direction::Ltr };
 
     bool m_last_line_needs_update { false };
 };

+ 74 - 7
Userland/Libraries/LibWeb/Layout/TextNode.cpp

@@ -400,7 +400,67 @@ TextNode::ChunkIterator::ChunkIterator(StringView text, bool wrap_lines, bool re
 {
 }
 
+static Gfx::GlyphRun::TextType text_type_for_code_point(u32 code_point)
+{
+    switch (Unicode::bidirectional_class(code_point)) {
+    case Unicode::BidiClass::WhiteSpaceNeutral:
+
+    case Unicode::BidiClass::BlockSeparator:
+    case Unicode::BidiClass::SegmentSeparator:
+    case Unicode::BidiClass::CommonNumberSeparator:
+    case Unicode::BidiClass::DirNonSpacingMark:
+
+    case Unicode::BidiClass::ArabicNumber:
+    case Unicode::BidiClass::EuropeanNumber:
+    case Unicode::BidiClass::EuropeanNumberSeparator:
+    case Unicode::BidiClass::EuropeanNumberTerminator:
+        return Gfx::GlyphRun::TextType::ContextDependent;
+
+    case Unicode::BidiClass::BoundaryNeutral:
+    case Unicode::BidiClass::OtherNeutral:
+    case Unicode::BidiClass::FirstStrongIsolate:
+    case Unicode::BidiClass::PopDirectionalFormat:
+    case Unicode::BidiClass::PopDirectionalIsolate:
+        return Gfx::GlyphRun::TextType::Common;
+
+    case Unicode::BidiClass::LeftToRight:
+    case Unicode::BidiClass::LeftToRightEmbedding:
+    case Unicode::BidiClass::LeftToRightIsolate:
+    case Unicode::BidiClass::LeftToRightOverride:
+        return Gfx::GlyphRun::TextType::Ltr;
+
+    case Unicode::BidiClass::RightToLeft:
+    case Unicode::BidiClass::RightToLeftArabic:
+    case Unicode::BidiClass::RightToLeftEmbedding:
+    case Unicode::BidiClass::RightToLeftIsolate:
+    case Unicode::BidiClass::RightToLeftOverride:
+        return Gfx::GlyphRun::TextType::Rtl;
+
+    default:
+        VERIFY_NOT_REACHED();
+    }
+}
+
 Optional<TextNode::Chunk> TextNode::ChunkIterator::next()
+{
+    if (!m_peek_queue.is_empty())
+        return m_peek_queue.take_first();
+    return next_without_peek();
+}
+
+Optional<TextNode::Chunk> TextNode::ChunkIterator::peek(size_t count)
+{
+    while (m_peek_queue.size() <= count) {
+        auto next = next_without_peek();
+        if (!next.has_value())
+            return {};
+        m_peek_queue.append(*next);
+    }
+
+    return m_peek_queue[count];
+}
+
+Optional<TextNode::Chunk> TextNode::ChunkIterator::next_without_peek()
 {
     if (m_iterator == m_utf8_view.end())
         return {};
@@ -408,35 +468,41 @@ Optional<TextNode::Chunk> TextNode::ChunkIterator::next()
     auto start_of_chunk = m_iterator;
 
     Gfx::Font const& font = m_font_cascade_list.font_for_code_point(*m_iterator);
+    auto text_type = text_type_for_code_point(*m_iterator);
     while (m_iterator != m_utf8_view.end()) {
         if (&font != &m_font_cascade_list.font_for_code_point(*m_iterator)) {
-            if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font); result.has_value())
+            if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font, text_type); result.has_value())
                 return result.release_value();
         }
 
         if (m_respect_linebreaks && *m_iterator == '\n') {
             // Newline encountered, and we're supposed to preserve them.
             // If we have accumulated some code points in the current chunk, commit them now and continue with the newline next time.
-            if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font); result.has_value())
+            if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font, text_type); result.has_value())
                 return result.release_value();
 
             // Otherwise, commit the newline!
             ++m_iterator;
-            auto result = try_commit_chunk(start_of_chunk, m_iterator, true, font);
+            auto result = try_commit_chunk(start_of_chunk, m_iterator, true, font, text_type);
             VERIFY(result.has_value());
             return result.release_value();
         }
 
         if (m_wrap_lines) {
+            if (text_type != text_type_for_code_point(*m_iterator)) {
+                if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font, text_type); result.has_value())
+                    return result.release_value();
+            }
+
             if (is_ascii_space(*m_iterator)) {
                 // Whitespace encountered, and we're allowed to break on whitespace.
                 // If we have accumulated some code points in the current chunk, commit them now and continue with the whitespace next time.
-                if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font); result.has_value())
+                if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font, text_type); result.has_value())
                     return result.release_value();
 
                 // Otherwise, commit the whitespace!
                 ++m_iterator;
-                if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font); result.has_value())
+                if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font, text_type); result.has_value())
                     return result.release_value();
                 continue;
             }
@@ -447,14 +513,14 @@ Optional<TextNode::Chunk> TextNode::ChunkIterator::next()
 
     if (start_of_chunk != m_utf8_view.end()) {
         // Try to output whatever's left at the end of the text node.
-        if (auto result = try_commit_chunk(start_of_chunk, m_utf8_view.end(), false, font); result.has_value())
+        if (auto result = try_commit_chunk(start_of_chunk, m_utf8_view.end(), false, font, text_type); result.has_value())
             return result.release_value();
     }
 
     return {};
 }
 
-Optional<TextNode::Chunk> TextNode::ChunkIterator::try_commit_chunk(Utf8View::Iterator const& start, Utf8View::Iterator const& end, bool has_breaking_newline, Gfx::Font const& font) const
+Optional<TextNode::Chunk> TextNode::ChunkIterator::try_commit_chunk(Utf8View::Iterator const& start, Utf8View::Iterator const& end, bool has_breaking_newline, Gfx::Font const& font, Gfx::GlyphRun::TextType text_type) const
 {
     auto byte_offset = m_utf8_view.byte_offset_of(start);
     auto byte_length = m_utf8_view.byte_offset_of(end) - byte_offset;
@@ -468,6 +534,7 @@ Optional<TextNode::Chunk> TextNode::ChunkIterator::try_commit_chunk(Utf8View::It
             .length = byte_length,
             .has_breaking_newline = has_breaking_newline,
             .is_all_whitespace = is_all_whitespace(chunk_view.as_string()),
+            .text_type = text_type,
         };
     }
 

+ 6 - 1
Userland/Libraries/LibWeb/Layout/TextNode.h

@@ -33,21 +33,26 @@ public:
         size_t length { 0 };
         bool has_breaking_newline { false };
         bool is_all_whitespace { false };
+        Gfx::GlyphRun::TextType text_type;
     };
 
     class ChunkIterator {
     public:
         ChunkIterator(StringView text, bool wrap_lines, bool respect_linebreaks, Gfx::FontCascadeList const&);
         Optional<Chunk> next();
+        Optional<Chunk> peek(size_t);
 
     private:
-        Optional<Chunk> try_commit_chunk(Utf8View::Iterator const& start, Utf8View::Iterator const& end, bool has_breaking_newline, Gfx::Font const&) const;
+        Optional<Chunk> next_without_peek();
+        Optional<Chunk> try_commit_chunk(Utf8View::Iterator const& start, Utf8View::Iterator const& end, bool has_breaking_newline, Gfx::Font const&, Gfx::GlyphRun::TextType) const;
 
         bool const m_wrap_lines;
         bool const m_respect_linebreaks;
         Utf8View m_utf8_view;
         Utf8View::Iterator m_iterator;
         Gfx::FontCascadeList const& m_font_cascade_list;
+
+        Vector<Chunk> m_peek_queue;
     };
 
     void invalidate_text_for_rendering();

+ 1 - 1
Userland/Libraries/LibWeb/Painting/DisplayListRecorder.cpp

@@ -233,7 +233,7 @@ void DisplayListRecorder::draw_text(Gfx::IntRect const& rect, String raw_text, G
     if (rect.is_empty())
         return;
 
-    auto glyph_run = adopt_ref(*new Gfx::GlyphRun({}, font));
+    auto glyph_run = adopt_ref(*new Gfx::GlyphRun({}, font, Gfx::GlyphRun::TextType::Ltr));
     float glyph_run_width = 0;
     Gfx::for_each_glyph_position(
         { 0, 0 }, raw_text.code_points(), font, [&](Gfx::DrawGlyphOrEmoji const& glyph_or_emoji) {