From 11e7d72686d86b3e900c0e9ab76e75d3922f06d3 Mon Sep 17 00:00:00 2001 From: BenJilks Date: Sun, 18 Aug 2024 17:58:05 +0100 Subject: [PATCH] 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. --- ...d-right-with-justified-text-in-between.txt | 212 ++++++++++-------- Tests/LibWeb/Layout/expected/div_align.txt | 44 ++-- .../Screenshot/images/text-direction-ref.png | Bin 0 -> 20751 bytes .../reference/text-direction-ref.html | 10 + Tests/LibWeb/Screenshot/text-direction.html | 18 ++ Userland/Libraries/LibGfx/TextLayout.h | 13 +- .../LibWeb/Layout/InlineFormattingContext.cpp | 4 +- .../LibWeb/Layout/InlineLevelIterator.cpp | 53 ++++- .../LibWeb/Layout/InlineLevelIterator.h | 3 +- Userland/Libraries/LibWeb/Layout/LineBox.cpp | 9 +- Userland/Libraries/LibWeb/Layout/LineBox.h | 6 +- .../LibWeb/Layout/LineBoxFragment.cpp | 118 ++++++++++ .../Libraries/LibWeb/Layout/LineBoxFragment.h | 20 +- .../Libraries/LibWeb/Layout/LineBuilder.cpp | 7 +- .../Libraries/LibWeb/Layout/LineBuilder.h | 3 +- Userland/Libraries/LibWeb/Layout/TextNode.cpp | 81 ++++++- Userland/Libraries/LibWeb/Layout/TextNode.h | 7 +- .../LibWeb/Painting/DisplayListRecorder.cpp | 2 +- 18 files changed, 460 insertions(+), 150 deletions(-) create mode 100644 Tests/LibWeb/Screenshot/images/text-direction-ref.png create mode 100644 Tests/LibWeb/Screenshot/reference/text-direction-ref.html create mode 100644 Tests/LibWeb/Screenshot/text-direction.html diff --git a/Tests/LibWeb/Layout/expected/block-and-inline/float-left-and-right-with-justified-text-in-between.txt b/Tests/LibWeb/Layout/expected/block-and-inline/float-left-and-right-with-justified-text-in-between.txt index c5cebd2cceb..2b3a1eed387 100644 --- a/Tests/LibWeb/Layout/expected/block-and-inline/float-left-and-right-with-justified-text-in-between.txt +++ b/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 at (253,11) content-size 300x200 floating [BFC] children: not-inline TextNode <#text> BlockContainer 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) [0,0 800x602] - PaintableWithLines (BlockContainer) [251,9 540x402] overflow: [252,10 538.328125x400] + PaintableWithLines (BlockContainer) [251,9 540x402] overflow: [252,10 538.28125x400] PaintableWithLines (BlockContainer
.left) [252,10 302x202] PaintableWithLines (BlockContainer
.right) [488,212 302x202] TextPaintable (TextNode<#text>) diff --git a/Tests/LibWeb/Layout/expected/div_align.txt b/Tests/LibWeb/Layout/expected/div_align.txt index 471d74c8605..e5d9aa6c1f4 100644 --- a/Tests/LibWeb/Layout/expected/div_align.txt +++ b/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 at (28,516) content-size 100x100 children: not-inline BlockContainer <(anonymous)> at (8,636) content-size 784x0 children: inline diff --git a/Tests/LibWeb/Screenshot/images/text-direction-ref.png b/Tests/LibWeb/Screenshot/images/text-direction-ref.png new file mode 100644 index 0000000000000000000000000000000000000000..a6cb860b88a2bd57516400d29fef9fba8281cbd4 GIT binary patch literal 20751 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>iV_;yIRn}C%z@Wh3>EaktG3U+Q>VS~) zdEY;pZ|QO>4t7%NY)Vm|!oSE+`<#_PvP^9HrpZjEi&ol5%Hd3RsUY}|2b0g>7=@hMZtrza%Yf9bxJzD4NI0T zHB3J@XLi<#l`A`M-HMWulFFO0PjuhEU)i5Nefsfg_4>L?xpz)Z(~tMd%zXLwU0X*- zN5>zR3LXbGJ{gV!7ZKnVu^Vh$Ao%8(MT%GuR zbAEn)Ze02*Darp&sXPxg=j&!fkW zKYseev|#Jjsa;)MdNDf={QUfU<*HRmrlwOBo!b}$L`05!czF1%VE?+z>D=Oa0U;qS zWo2e%yXQ`sA~LVy5hokZhZ~{he#g)I4+?>8!R@$mWLseDP z#ohh;D!+!c(c33YpY9$NCB?wX%DN_EV^iVdV}`l6Oc#t@6_F^RN?(UD z8wd#tpH1!-(>1zR`Fw8Nc4bg%{VvuYydg$efk9JKGw05Zi#FemN#_@QeHF^UCuhS^ z^5^QGCnqQG`F1ONP1M$|+TY)1KAk;#_R4kZ%$A3h`pVnYNHoltA;HDXZCUzCBr`Kp zL`=-6`dg0Qe7ju5B~8EI@1Otm_4UaUCNLbhx3{|T_1f)czT18+JwMNu;mwVW&6h3( z-K+bZJ89CSGZ*Ewb4!jtEckG*`n{mEbhg~)n>o`?l|6m`{`@rEXp5R32HoQNX;)SR zKC7Pl`}XXr^mB7M&!!!IS@LPQ{ol@}CZ+|+$xPzA@2(RBXR3{B+!;@7NIYy&@`&w8&7E;lC5`>eaPj~h8rQO?8Y4i0;a9U;ffs5C! zwVh4dt#|j@HMR%a#2#H*>Kzgi!tn0k1ktBYpB~uC!2aRJ^@aw9XJ=*}-j;j&#=ctX z;^*f~Z|}XRakTf=1|=h-Nxyzo8Q;AvIN_XG?kyKLx3-ym{r&r2TGiInoSC@%$PpJK zBO?xee)Id^KYtEp03|1-XC&Y`O|0A%|9(EN z-okz3xP1MYMXucqeXpm?oqM+Uylwiv`?srT*U3*c*K-XV6P#CQ9)ZA>%Fzc%bV9ddnN zMbXDcuEk#A*DrGV*UB2F@woTPJ?+-tw_wGJju$U7;%dK!PMSRV>_rLx+=#@@H_ybb ziedCfPF9|6o}YGln(i}>iRz}NraBQD7#c2I2(bJ0LU~^0Gs*2XdsluxHC0>2rb1x( z<&&FIPoH@`ZRX6(dF3G?B8S^}H;2pzxwGRq!;#acyDwY_cyNn@^?)X`HJ8`J*O#{-cguq8s%vW^8J;|QHfi?k(=RVCw=91*Cvu-U zqsMdg?d7s_H(a{DVS_eQm0 zJ7+37wMMXwza`NKJ&G$^_PMqjiRb}=2+uP*7zrKoyi|_ur803SM(XsC8 zYrcH_`~AMUt?k{lcP=b+zI)BOz+N_M0+m z{e3@_N=r+xT)S5E^Hb`oRjVutAF%`m2JT%~erk%Qh?v-RpC$d@SFrk(D5!RP zxzgGUoaF^4?)HB#Z>I>(R+pSH+X%^;3_qVt_O~c_z`$_4Pd54PuF|~d3tiv@5|OCM z5V^0$a_Q2gX;!+ry_Ut#?t~on_4U1S?brDUa?`C0b5?n7q{Z_`RgyXP5M}NQh{ML)N zZh5)7vWn})JoxkTb4Y0D#96aWO;B`Z;Nap)`tjl6vkv#`TTZufi)(0UDOp=f@4oBj z<;CT1^ReY8OWEo_?9t-LifA;}R=F9_HnjPu|#= z+}YK&=}Mcrw0WM2sw!(k@Nz#v35gqvKfl(&(j3@z4_9koOWbvBP?UWaINapgEhhPX z&*#2GiMGObcPyC=7#^RQX?*O|RPD1RiHQp%)zsCG+r0br{gn3l9andkybMw{G5O+t z`Sdhhg=FZN`8ChAIF4aE1v$NQ(q-0BUd{T0fIa5-bQUNNk!E)w>~k zEiEnGqN8V5=DfbPmMuX=P3_yRI8fr-xjkZ5Q|k8H>!lePJv==VA0O*|w$c1vMf2+L z^-KjH9yl&|y#ATG|GXz#ug4`{T^0J~_V)8S_wU^CDJ|VR}cc1_J{XYHgudga9DhwOS-^bmndae7p*1z`d=5&4r z>#{eXw&wQxb+eZ&QQ59OzdGyjaSIu~d)4o4laiCqCY#MZd#Unn$Nv5H(c5w!et39T zCvMM-z{PGe-`_nm`PSdRe`i`2tC`I{TjN#h=*W0sqhTXBQHz?3HtXe`??+j8^7F{2M3!g-)ub2@Z-y6|FfTbeSI0WY~Oz0;4N3$ zrcIk9OtVB5yZ5KPy|uMR!jOqUUS8f?{pI7whp(&*mb9y}V0ifOq0bx(!2`+h*;!d9 z&YVg4^W&q6ii$zvA(m-+v8UMOYYv>5X{@{TqHKMA{ijc#PMkbx*k4(F|IWJ;lhys7 z?fd=iumR7f&!5jeK6vn;d8ocaWu@h0b$>N8vuV1~+a}DIapKzA=zM4tnMXu@u;k{ym#-Og$&=hdA8gM@1}{$%AVa*`MKiN%H?N1t!r4k zb}jGhvt_Zn)~@9}kZXHnsrU31D^@VvX;#!eb^3H;HY4AIjq4d18PCi#ZqL2FZDZ~4 zvS;V!W^b!rJh4l9>qF0!6qT=Ezoz|4b91n|y(LpvQc^O_%FgcJve#ceel&ESCnPL< z^27-R8=F7NUV{pho)a8gg%M z3k(lG-YKjOYJ3zH7N$vFZz{fiw(5F`w>LLKeBIBbkkQ(*t1x8d=04qg-j3Ny ztb2FGtJT}Lo9plWBE<0d`FV8}m6jt%T;l3}rgn98>1<7GUB7<)&t1g?8wKT;zq+}& z6#V&77+3$d)XU4Oqod=3Uj!)oCe2*RprWPKm3X*~@tyhKvuDpf%f7xv@imTt zfr1w=UflER)oK-W_1TLJ=iT~nByICd>yT1D2PtXkn|msaqqpT4x_8*DT)8swU=u4t zOIzEgi|+Ct&lsQI@${wd-tcuX4<8(CwwUWTO*c9%Zu^-tKA-ovg6h*<$(jtF-rmXY z?(B3p?(~daje?_~M!^nth1S+qhMXH44(3i-V%?TFqcO0w)YLfb%!7&Ua)!&>zizv? zX_L{TM~@^di&V;X&%L|5oLTnrxmBxI>s|%dY~RhivmGW)o_sfNg}7dfM{@GwXa)f> zF}I>3BS%L^hvQXU=k5Q`S+hoG*6i7mMkyR+yYKEf+1AG{V`gSHtF+uVJtbwrym{yL z)&7pyUuS#t=+V5tuRIwpRlW+levwnVw)X$u@1Fkt<=W;YF9Nok8s6QSc=WQreed+? z(5OEqru@bJ3zjRk81!I@q9-0nNl6`jecSeI z&GGf+_4V~-VPSdj`t|L{x|#a1yN*0PJv}o!+jx10tDD=S=g+UF=a!^Ou(|iiDC+2( ziFtEhJTG)F$Y7;;e^Q{M1V3Ny-pTFCa}M@406u7}w3@x<3Rn-{zH7a7l9@r<@Z1h+qb+?D*&u?6J&qWof8hFzFr1ZSVm z-@0vEP+;J}R&H^N`hPZ|p`lBbEi+0vA;8eo)Ffe7v%~VyMbo!M4-c_+b#g4;Sp%G2`2}vN?Y0ZHW&` ztR$_=bOHkdr`_6+^!Qlsk|j$R3SM2&WMXEnF6q;&tgJjU&-V7)8y6NjYiepLT3PK1 zFJ;QPyKCy6JvNsvU1HGC&`3DiCE6ox-j{iK8KZ!NM2029L2%#nS~nzkmMYSKC#7N_n^Yy&cbC1$+DZHgYmDcV1h?o;rPcrcI^M#*G_u zZfQj@-rcNy_a`&BWP=A8k1RfCK0SZmPqoCvLC>67 zGszrCj%Q|Knqi#IcY9lI^6zhN&&;*Hp7Y&pv50xr;jR-WJYKzieYPauoj{A=PR;%E z=lzuxKzaI7xz+tIyO~%T+U4sc8W@?`1Y~5+O!l|qJg_bI_KMZ3A0O!y&bhyD?v5Q6 zn|rbk?tGoLH+$F8rK}D5`~O_h`Fxn){>}#dd0Vz^`*bsXKBH{+(O8?WS8v~*Jw2|9 zv*G9G=fXllPDx2h`uqPFxw^VeyS3re_3PJb-f}raMMXtyNMJl~|G%ccjPdu6$NiC8 zG6XLa&fTlmlvr2bYSHqke2HsyaA^pexJ$v?W@ba>*>kVMA`EtSez*dGQ7v1Gq6HZQ2Ju}aC z_LVCkLBYXyuQgn`a)sf#8~3hd>1SuLI^^Z)?cBL@j$Q4n;^*gPKCKpND5)!|2G^#% z+}wwcA8&7KV@pa(k}ynSnL2gq9Gl8ZLDFHiKOQt6*vhbE!v=u{UTL!(y%S1~8}Jw# ztXj7&?O2bb&8HK}&%SK9cP%eFkbby1B8veEIUt?d|_v`0>`BJjI$hn=b zH2VF8Sm`j^zh5p_R8}T#zL}Amn|ip77t}<%ymHNk+i&;$c+`F6>eayL=+jS6PhYue z)uSVw!Y;0^Uw7Tp3|jo*=I=)D=g0ouEN%i?PhZ>pL87#`)NW8wbw)l2a z*by$6tEJ>_ zlHLD9U-yH?yVA@;&z?W8uAy?wulJK^~J0aB!OP4NHQBes93o8r$dS{hcpNyqb zVWHvHty|5nvL7}$aq{HDx#jl~zrMN(8dBa{{rz6ZTs`aZcPT$VJq3;MtqfkiDebJ( z>C~?ZPM|*NMim2t3p_S4{LE|CuV21y9pCP|=N3A*8}Ky$_)+mWQey4ewILxP1rHCg zo||v)9~vsUa^*@MNh6kq1q&3~_~rBZDoT8J$LuI*T(n4O)8@?^Z@*o#WXXpk!u~V- z6SkjvSrQZ($ys7^_wK`jgzdL=zg~S<5D*jNQ(bNCw_NzJL4noW9$D*YD^_TD=T>Yg zeC!q!6vSXr`bwm+vC-CQ|G7_}KFzSL-gbNALg)6NpdhD`l9KhW@9rwquB@zFvUDk9 zKtRBOE>Ud>^E{c$%X~k_G=f{bNe36XcK4Lut2}PNbJ(K%-5f@VhQeoOB$J<>nwqw2 z!-fkwlP6D3mMi~T{d8*homa8DN>&O=N&Q;kz>cA-o1WzclY{gc>}vYADmC;{sFaWlHcCin)ehmDe-bo-ja3e z&Vj~lm6Vlx%kS4-KXK-a$!lLPuU#vvii?VRwrw-}eB;{MXx;-JK0aGQ+M(?L)w11x zchRIN@Spgmz@2$D?^gE8KQ6GSc)$01M{lob|IHjT@8_k;|Ni|0)r*JQ`8QX5Omgd$ zO5Im(?4R}h`}g!`XJ+2WG5f6fm6es%;rKC@Fz95QcbyI`8UgRgXaryk4Xpk3osXYC z@6HpnIzaX`fgB0b1Rh6wZ+%dKP7~A`D=W;+y|tyYr-#Q_R$jjTa_O%hKV~TVcXoEB zq@)PQ$>ptF;|6J{OloOiDK1}i&iwB>2F3>u+vV934m2>bu(N-DGTEPLL+)*}SFc`y zhGTDU&sW#eJE!wFs^HzO$oI4IGBX(;oYvoOa{AAQ!~D-)&tp+oyLN5Ox0~q>$CC=h z{cFE|`}S<<^f;!3e}8_qw6uVRPEPCZ_X!M~xYTUThp%5v()KTl6iK|#T3<%fbG1?@B!NIiSt@3Q?!a|NOsO12A6IsA}e9jj67<+W?qGCcFO zO?YQ?b*uiom^~F2=Ulp4T5>2j{&E)DhHSw>!wJj?@Em>-|rsBgvcE;~W?SM%nwF9;u{}jaxY9}g9 zQ&CsneK$ZqX2*nM$K19vDCp?)Jb9AR)YR0__f=$m{Xfgd$Vd}&bK{f~0&mK8S5D@Y z<+rGfIU5683y_nOWAggltaB$$DA?N0t^NINW8L4XuI}!;zphSZSn~PGTCi7s{rmlX z^1ONP!t!2RSeTccv@82?*Yf#wQu_M(#-Jf53L6A@*Iym4khGoo2HY4ZlHR$kPFGJa z?a7IWM~)m3kd@7?%)U8mmej0Svp6_74qU&!{dn}HxLqZkr>E;*zIH9mPq4eE=g9f< z>(AeMams)PRFy_V%-DHjzy6(7RknzlAJS%W!M4iatnGg8u-shTH*K24E?3cT zT)uu!cxBZje^8n6B<^V;i;A_iw7$On=lhS2cJH>mC!Cv`JIB6$U;L*(GlbRsKKyvx zKY7X&7f(;m^`F0}-1m3&xBI>ChaIRRb>i;%OCmb$hZ_?mB-#?~UT)a^IIQOT-SRyj z4so}%w;Si*vneVr&X1h;uH>Hit*=+k-h1-WbKm?)^J>1Cf35}fhbLAa&W&cCG=ILo ziwn!{vbT@Eyu5s7mT9-Jy5F5iHg`XM{Ad_|{p!`PR{s8Yi^{G1lHKJM=G%3YU#uAI7a=gyd&MXeK+-HmE~6x4m}ju()Y&erU| zx@*m^Z@2STu37V>UB2$Y?)Up1?|#4U@csMq>;GP_fBV@6ocR7e-NvHu^yyO(5s?W~ zr@BT*OYi@6bv=Vc@iU+K&p$5tyuvj#RrU7veDllP;(97(X4it`R+mNwhJ_tl+;8`) zOh-$rD}8?Lw)3AsLDcbOc^$ul-#nX>GYpgO^vPNlu{5k&waV^na%=Wow~P#p>}zWj z?d;|qZs-4eEjoW=@^QXvYopUO`}gnP|Fq_AW@e_LyZW`Y(Z|2Myj=O9k)4B&?;XsY zYwt5p=<4EP2wN9(a!cmr4L5WCe44(W<-wbqn`>ts=eoXS<*Qd&ZvAq;E-oy-zP@v8 ztEc_?RdwprsUJ7b*F7`zuc_Iybl0QDk9WWI_x9$l{`MyE$A^bH5gQsB8XBUfxq}kO zlKo$QD}Bk||5vQx$B&9-{`22`6aNWHpLg!uNv~Y_ygW;N2a|&tJLq_Z+ye&{_BC#;EC{+F@%RnD76|3-aij z8ykQ8_yHQDulxV|{`X2bP-AoAzm!bgj=Op0pbp@Mf`^Cl`peUgc8OM0R!*EZ@7%7^ z*Bt!(>5q@~vaqmBm@>r$Vq4OK1C2Jn-)uH9GrM%*f2JH>Jv;Dgd(g1LI7mGPj zc=^4*lQ_tCJB6=ayH@n)N1>LsHYg=k{P}pidg^lZpYL3y7(ogI&2QekSy5Rz^S_vW z+?~~rZ7*KCcI@Zp=f73Y_gfY|V(GX4XW{ATdEGX7oqA=}r1#4oxy>)kw=akEDT-vF z<-oxuo|CI;?{vN2_q$I#uA=e7hk|EkXS?U+z0*B?dG0y>sbv;2d(T~-Tw||l{;Q|@ z^G@g@hud2A_VYVBILhAMI{NbR@|mXDV(NZ#cBoeFdi?lt^1gMeSHCWuH}A=A@l0ND z#<~;cJxyof+O@sYrisn*Q{S9+HtF-Tvuon^?mGXuB>lmG#%HhZ+=*Eiv-8l2iOQgv zn*BeH>UZ?^Zk=*GD>kh5_uK6uVPT&R^V@&8TYmrJ-SYbn-@iY<|KIEV-zw+*TL1~% zyxE^V6>)KKoj7%B(b~1W@qb^1Gt99pUUvR-#ihzCi#BbV^!E1l*)LnU#f4x6jE1)M ze%xBz31QwabCOo{*l|9WhZ+QHC`!J11pcUY5A|=8vcP z^_&N8ZchJg(l5F$Gx+M&t37hIvpPCBX3d&qQT({SSrcV91dH&xs$@6~vsPHa5 zdi;3#?fKKDiG6!>^YDj?pKJ}^zLhcT`~RNh={P?=q-5y?EkA8f79J9CT>$ImY^&#o~$uB4TPH%6oiHXUD8#g?1b9Ke_Jj*=D(qUR+$v zBV#dvk(te);DJLMuk^L&l{PPbN&3&X+Z#TA_w+=Gwx?m>`JG=8-rn4A-@d((W5&iS z<&vGP9lk#9D5y>F`NZ4hkg#<5)k}ChK z_y51&d+qX$o||iJS@A&up3#laE z9a&%h_qE*D$0sH#U%7tWxb9B{6Em~!)SaT&!7CJ>rxT}5V{>qH zWYi8@lW=E8q0elyvq7?$!|AtzWRYg#Bl*A`2bU$);L}!J2S^pm|^+llk@-oN$29?0*#r>cxtsR=jNdo7Z-#2 zVH#RmM)yB|`t&K{9(4VMej2=Ed zPmYSmZ;7e<^yH+&@eH@Rx_y;478W;>BO!Ca5+)e}%5FUj2X6oFlT;A`MaIvQyB{+N z$ji(7&9~z%VUd@Vlx%5l|Gs0M?`*S)6DKAvd;IvZ?$qv{o+bPC-Sf$20o58VOWxlp ztiRXa(edEN$H#mA{dzrR$`k=X!Gp6*v#UamL)SwwfNZ;Y^QO)BJH^vZ|NixB)_Um7 z(3iQ^S`0gjo^rAC%N==HvMJ}Lk(%!;7KgMnwea&-x-EInd?TIa$$$DO0C{vgwJ5${TOLeRFTG^os~_P^(}i-(dsLeDi`` zyJkrmr+qk~+#e7ZcW?1Nn;#FFq0?Ct_oZkufb!hrJ^Zp(S6)A!=QGo&_0Su21)Ycn_jlpD1lZk#_~p5gue|8?v8qc;{m_v4eZIdQaGd}qN!ruy&Y z^~XOwJ*_+4oTvHDotRatSFc>Ze)+zA^E@Z3eS97Nf0z4T(2}69U%&Rq+s`|2z`;B9 z*Q+Y%XvItR=(VbT z^G!)fneh@-*F9BIR$jS&z4_(s$t=+66PI6}7{mY9)r$qVWY&tH#Ou4x!)n}FoXTzB@KI`K4X07a-HA_lN zCxXEtBSS;J_KVaBe6{ z2jxIW3o_s8{MjhTf;7172AKxTu447n<@zlxEudQQbS9`Y6x?YX!RPSl^XJZvj)d#$ zVnH(#k(<+EzaJ3RuKeYckgy=S;i0B3*v9fU#xP^O$W1NVwwXmmMHN`h1@$D)&Nkm% z_4Sq9)0f}BU{9{rX!yFVC$avt?RPCGl>+M}#U7uB_pP#pN&T{>qKY#9I zm6&%4?8c(>4;Ds%zk5MvAU|NYTkQu6O)Kjk3q{=|NXuHf39};x(KiP zFCwC%q(G~H?|+{n1`7R&*3HusCEUs-=lH3E`l+_IvJD?T6x4m+eP2*Oz#%9|Xu03q zM-$!UKF&!0^5PYt%xS@ez zJHz2%sf3f0RJUy1dhybwrpqr=uC0m8xwmKL)z#tOHIPhB-hTTo6OaE(?41nv%3sYe z@A&vLO_;3iZ}A08as{-I|b zbcm^_y|b%p(Z-DzgMQ7<-^aLM?_Sx4_xu0fTf45Ujm^6fw1V!}*Vmxt!{=vbm9@0E z9_&heoytEQ{l3!2^YYHw*_0If(Vdw0jv)3Y-=mY0`z+9w83U%CGO_xk6j zrfNT{j_t4Xs(zFz(Uz#W9PH#T-H3$r*zj$MRpjP0-j^?5GPJa}f8KrnZ(lS z*mplr>R9sq3H9pe@hxBwJa?;@pYQemzw5q^j^A-H{nqXCKg;UOtE=?>UR9sI9Mx_#fmK*KQE*VjG0u`wCkaoX9`#PsUbs~HA~PU~;K(>MR+Q~lYpJbaz~zS*|6 z)Bn8^E_uT{Z^DFxN|v?eRkx45-I2EakyLj_hsS%K9d~7Sh_dlYu{hM$+Lo4<-gsNa zCu`M`C~@TY@yAb}rasHf&0V@^Q4_bgUctXVm8({*^6>RdJuyL%g_YH?=0`#Ck;U1& zHe=uMv!SkYecawteX`aJ9o^l^YHG)7@~^Lpy>jJ>LH)m)J$v^2cr5?_#1ze74lXXH z2bY$5S5#L&e*ZrI<(&nOJE#Bur2oHZ{(Sk<)AiL=R9Kc@_Vo7V-nVa`{jKwRtG@@u z#-9Cr-k!hY&Jj`Y{-L~eu7&U!!PC$6YY#&v5F4gX7w2nd-hG#^;-6o{U!C3AkK-=f zEq(L$J=ePbxt$#ycY5EQVmM;T#009zLGdY_zlYHwF;Q{Rs~B|<_%Cr+MZ+@bgO6DYd>JmjxuP%tu@1kwT8UE(v_toQ%t`hWcXwqHe> z+4(>VRp_4>cp|2(h% zr=EPQM^R6&@6)HEK3VHDHod!Zr4NAC!hSw$&M0ABrt|oC|MUOIY3bKNeI@4haf6blKm&Hg+Gs{ht6v4}0tSe>NaDsr%1kNw~MC z^2o7cpN@*hGtApB9A@Y3pL>UU*Y3W(e`61R-LESsIC1yI*qt1AO_`V`Oqz7)Sg-Wi zLw({3(zIUYgc7UJ;1l-xWD#a{QuYW8>_x%8Skz7 znk85FBhmQoojW`wXLkGh`m#PaF;O|^{ytgoFvRwYvi{H0Hs3sx@YMd_V|&n??Z1Ej zZoDn~_t^elbN;@c$JVUgwM%OG<&*#aod3Te`8eNa{bvl--`+4L9Pg8zGHu$YZ@2R& zPnq)Mn)plk@AvENUteFZuBgcP;z#$d=={B@``*8_PJFug%avn$7wmq0WAc5`lK;1b z1O<0W-~Ebxw-R58&tlNf=LJJ4gNhFca@B7P8SMUk30{!=&@yKC!S}P+&NQIzRm#>= z`2O+Z#~aUY+=vJY3Oe)U)6>)W5)FSI+y7H`baVvGkn&2KUAT0Ksl=!A<(X4E8PnuJ zD-!?zuK(X}|K}lpn%(tES<9l9A3rMQRX&q6%f0oa%+J)+G$k#~sP0dNkdV*}gG46p z>3UC_`Rx*(otY_jKKj?R{(k=8<$kF@K0E}K$;O3`Tn>CLKlCd7W~}sgChh&Y+WWO% z{M)Xoq*Szh*X!j8dw)Kg-O}31Xi)gb<<#lZAHTip*bU#ubOvc3(}Atw7szlbdVC(Mu`*w9d9*&yqR z#`ay`-`zcZb91_7^*5dRKab@bv&}s*7d+2f|N585e{Fqz_B+g}{`WiQ)lD5x4F2uybCM;%h922!Ei>9_4=U?IJY=4&H(Ljij2Kn@V4y{Xxg&>?}x+u z=5PGVV;E2Le%j46p{0do`_ISgW=~xgykX58>5?9S#^1clUa#N2?(?r-uh&QJEK*ff zRsHk+|KI(LXH0eP;oJ#Ta_`disOadQZ#JK2_&(7PJ#Z@j^r`n;yC9{| z+IPPgj+mBmuDi=na&P9U&t?nEZyi1Uuz=w@)68|5FLp>WpOH2?a7S@PcKxo$TuQrM zAAfRka(=|MtMkt(tIs>fYfygiRgG^?+kbyPZ~t8W-$Qv0PR_&=6BI!y7Bm{d0P6lE z9pU(_wmMr%L0P%^{r>;+Km|Ey*He$2?X6tLomUxBpmPo`Zf}Jw|WDv8sV`pu)MX!5YV)gU#{C_Tmg@(%ReJW~dZ7*KD zn15y3mfYN2P;H5Ht$CMUXJdDA#DdybF7JGxBMGqn>cr}4mtVSQ3;mkT1e$OZWPAYGQGG6V_t*K8 zl8^U&{B&C1IQbaQsZ*zPrvChSzP@k!cJun5r{nVq-yd-W^|DXsPMbS7H}B`0r%#VQ zI@+E8^W(g0iC3@3*ZaoB$;H=x4ULbTXP3En`}3>v*T4Av`m@%<+dKK(9Ls5+?|TFX z3vb=J)x^Z4q-=hohf8_&hmzFqt1Q4+(ob+M;6CLfou zDp|3#h?SM~%GIleiHBO^w%6|d`fT&MRjZ6n|9K?7|3gH0{J-x{*3}5@>gee3&d$~r z6%_@oqx$^(d~j%}YgU$)a~luif`tnk>wiwKKY0Fp|I^dcg@uHgF27u|bSZ0m{C)Yg zO*Oc`1DnF02=ZV7Ea!`J^Q*|R#w&w`+B>$xVS$b`~UCX`2w3+|siF$jy&)(G2(|l$csj|yeJZSlsd(!^>9>a;rNk&2n3_e-f z;p@({ipN=G-F&@%|Gd}hcI$Z>mA<-ir}Ft+&q>057K+^BdS`mg@69mHzLxUy)6<`) z!uJ{dQTsjd-M-)NWGbIb{BOz#+9<8rEv|p=*VosRRi?)jdHUIYy|N~HyWYtkw=F&A zRX&?Jr~F>!{}0>iKQzmyoSS3$cwIC9y$4(#lXC9uu{6J15}dd5=`@v-udc2x{&q8+ zKmYz^JNxfc4ZM#!J^JB%=W6M=Y6j(@0Q2g{r`EMzuxZem&>4aKhG}D|5q|8ma*u(?RStLB6k+0>g{;Y zG^gm4=Fac;s*UUa{n3fqq9JXTa{@Hqkho*ZIqAo;!jq?JMlkqf+5dg%KUsy}=7Ynv z_3`s(=kK$;9$#M@wl-?2%FXKcd)u4)r1N$ts?V!P;vz1F7M*wU zcK&|bJ+*sk|2>rZsVUt@QW*DO$Jd)hZBMJhmjT z?)UBceZOb)+yDD$#Nc)!qC4Jk?kCoLBvB=bDI(N}iw2 zS-)2~xni~T+Rw)-jvq-;bzqpWWn2FJdGdc>xOh!Ns5|6Jb1f?C7lXbt}hX3CFXZ73d_v?OM zS#CGAzwXOo&q>LBmTJrAmPJkK%zeH#di%5L`@iGF^kQbHdQW@uSpI*-zAsDlpH6g_ zJIN{@WAM)Eg@mNS<>mh8Z*9%~ygL4`R`IhlCxiWMr;5kd7@j=x?Wc79o{3NHGX8rv zjkj+-yL^qo=JfOD?(8i7yf(i!yTZrsiG=mnE5XG-pHAnm2klZRmsW`Xed~JBhlA{( zO@-&n_kEtb=j*j-&obZN(kn~v%{XCb7&)_v^}v#^x3br#K0em#Y4j&#e&y$9PbYcn zoqTk(yZyWMmkZ95RX{n%RXlcz%1qnpvPnGbn*KH)UCi?C%;=RifA;aXy#J^Bq6*B? zW;q^4RRNY0-`eEf+M*M?%Y@(dOF-C~h>7QG-xVJ|_ztv75#&uv*4O8(-=8@%(|EE9 zD16LvZcNY&F56pXJF(WTqgU3tjOY8u8>!Z1Z#*W+lw5E;{59|9Ce_(yxn4$A9p6%( zgihNS&d>HbW@}bxT;Smt?bWV zF8lZI=Cg9+`@vo~A*I4K@x!qXoo_xLoPE~4?a7VQ=EtvpKA$gt@A=g5xSMYu-<<5& z@aEmWW`@X3DJNHaPvvn+cq6jrT4nO0<400d92jO?ajR2#`TuLCSF`_N$)78&n=>7n z8;|TNeQi|x>&u+NW0Hp%kGQbAPd+An!hpmg530ma=DxoI- z`R;pA>gU@V$?N@mQmoZao|$P38vRjSzwcL;m`=om$jxb~mzViQ?ysv&J3DLZ@zM_m zZ)dOHTXZ9_J@xamvxgl^Cdrgu3G{U1-*)`=^XTn)zD8B{eX(T^Tg9J!yPbbNx!-o0 zYqwbHyE{97K9>J)vFCsFUZzdElZ$R|%k?btoqB8TUEA}sp7(wPc}7%#L2c_&@9EE; z+yAdzx98KTGcyd8<$qmhFM6@C{n-iSewlme=jK>O?kah?qVh%aN~!#PKhu`^&Ym_s zzRuEny58A0H#dL2672uvfNJ9qoj^Y#CV_x(JZ|E$mY-HAIp zi{1AwtXXvYhTZQslT%MmdwL_e|8M*4vUhi8)_%Wh{`vX&`)}XRE4k!p`EJK!w^+u^ zdFIEg)zlvSee?P-zkSUQ>t_GIw=UoRl|Ik^+pqcm&xiXl9Z=bte7q0T?6!QhV)3V$ z=WCwD|1J2Zn{#)UY5ty%ZlJ*6um5rQ{p-a}+jDMC3XiWfwJv)j;a6+X|JU{T;GQX$X$H$F{g@Iv(AuBfn z!wNwK1_p;-21W*kEEWz1h5%&;28ISHMkWS^D;xq03=7;E7#J8#!5T)Dj)uT!2#kin lXb6mkz-S1Jyb$=KzbeN-r(=iP9|i^n22WQ%mvv4FO#o4wIG6wc literal 0 HcmV?d00001 diff --git a/Tests/LibWeb/Screenshot/reference/text-direction-ref.html b/Tests/LibWeb/Screenshot/reference/text-direction-ref.html new file mode 100644 index 00000000000..db173eac534 --- /dev/null +++ b/Tests/LibWeb/Screenshot/reference/text-direction-ref.html @@ -0,0 +1,10 @@ + + diff --git a/Tests/LibWeb/Screenshot/text-direction.html b/Tests/LibWeb/Screenshot/text-direction.html new file mode 100644 index 00000000000..e7000d0c8cd --- /dev/null +++ b/Tests/LibWeb/Screenshot/text-direction.html @@ -0,0 +1,18 @@ + + +
hello test 1, 2, 3!
+
hello test 1, 2, 3!
+
hello test 1, 2, 3!
+
hello test 1, 2, 3!
+ +
אא aaa bbb ccc מממ
+
אא aaa bbb ccc מממ
+ +
אא 1 2 3 מממ
+
אא 1 2 3 מממ
+ +
aa....!!!
+
aa....!!!
+ +
حسنًا ، hello friends مرحباً أيها ا test لأصدقاء end
+
حسنًا ، hello friends مرحباً أيها ا test لأصدقاء end
diff --git a/Userland/Libraries/LibGfx/TextLayout.h b/Userland/Libraries/LibGfx/TextLayout.h index 7e452b59b2b..d1dffb198e7 100644 --- a/Userland/Libraries/LibGfx/TextLayout.h +++ b/Userland/Libraries/LibGfx/TextLayout.h @@ -44,13 +44,23 @@ using DrawGlyphOrEmoji = Variant; class GlyphRun : public RefCounted { public: - GlyphRun(Vector&& glyphs, NonnullRefPtr font) + enum class TextType { + Common, + ContextDependent, + EndPadding, + Ltr, + Rtl, + }; + + GlyphRun(Vector&& glyphs, NonnullRefPtr 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 const& glyphs() const { return m_glyphs; } [[nodiscard]] Vector& glyphs() { return m_glyphs; } [[nodiscard]] bool is_empty() const { return m_glyphs.is_empty(); } @@ -60,6 +70,7 @@ public: private: Vector m_glyphs; NonnullRefPtr m_font; + TextType m_text_type; }; void for_each_glyph_position(FloatPoint baseline_start, Utf8View string, Gfx::Font const& font, Function callback, Optional width = {}); diff --git a/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp b/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp index 4793d20eef4..64514cfaea5 100644 --- a/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp +++ b/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 diff --git a/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp b/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp index 7c3ee7ceff0..29d6438474d 100644 --- a/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp +++ b/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 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::next_without_lookahead() { if (!m_current_node) @@ -176,18 +209,29 @@ Optional 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::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) diff --git a/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.h b/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.h index 43fd444db33..bc5ba0c0c81 100644 --- a/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.h +++ b/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.h @@ -57,6 +57,7 @@ public: private: Optional 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 next_chunk {}; + Optional last_known_direction {}; }; Optional m_text_node_context; diff --git a/Userland/Libraries/LibWeb/Layout/LineBox.cpp b/Userland/Libraries/LibWeb/Layout/LineBox.cpp index 360efab2a35..22e0d4cd642 100644 --- a/Userland/Libraries/LibWeb/Layout/LineBox.cpp +++ b/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); diff --git a/Userland/Libraries/LibWeb/Layout/LineBox.h b/Userland/Libraries/LibWeb/Layout/LineBox.h index bc4debde8ff..f46fc1b39ce 100644 --- a/Userland/Libraries/LibWeb/Layout/LineBox.h +++ b/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() }; diff --git a/Userland/Libraries/LibWeb/Layout/LineBoxFragment.cpp b/Userland/Libraries/LibWeb/Layout/LineBoxFragment.cpp index c60e2919369..52a1272054e 100644 --- a/Userland/Libraries/LibWeb/Layout/LineBoxFragment.cpp +++ b/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 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 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 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 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(); +} + } diff --git a/Userland/Libraries/LibWeb/Layout/LineBoxFragment.h b/Userland/Libraries/LibWeb/Layout/LineBoxFragment.h index b5fb84a2aeb..4c343c17850 100644 --- a/Userland/Libraries/LibWeb/Layout/LineBoxFragment.h +++ b/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 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); 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 glyph_run() const { return m_glyph_run; } + void append_glyph_run(RefPtr const&, CSSPixels run_width); private: + CSS::Direction resolve_glyph_run_direction(Gfx::GlyphRun::TextType) const; + void append_glyph_run_ltr(RefPtr const&, CSSPixels run_width); + void append_glyph_run_rtl(RefPtr const&, CSSPixels run_width); + JS::NonnullGCPtr 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 m_glyph_run; + float m_insert_position { 0 }; + CSS::Direction m_current_insert_direction { CSS::Direction::Ltr }; }; } diff --git a/Userland/Libraries/LibWeb/Layout/LineBuilder.cpp b/Userland/Libraries/LibWeb/Layout/LineBuilder.cpp index 35bfc39f30f..9f8531d596e 100644 --- a/Userland/Libraries/LibWeb/Layout/LineBuilder.cpp +++ b/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 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(); } diff --git a/Userland/Libraries/LibWeb/Layout/LineBuilder.h b/Userland/Libraries/LibWeb/Layout/LineBuilder.h index 7b3f6ff8aee..178c45bac14 100644 --- a/Userland/Libraries/LibWeb/Layout/LineBuilder.h +++ b/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 }; }; diff --git a/Userland/Libraries/LibWeb/Layout/TextNode.cpp b/Userland/Libraries/LibWeb/Layout/TextNode.cpp index aee8cb6720a..39ba6c9a434 100644 --- a/Userland/Libraries/LibWeb/Layout/TextNode.cpp +++ b/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::ChunkIterator::next() +{ + if (!m_peek_queue.is_empty()) + return m_peek_queue.take_first(); + return next_without_peek(); +} + +Optional 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::ChunkIterator::next_without_peek() { if (m_iterator == m_utf8_view.end()) return {}; @@ -408,35 +468,41 @@ Optional 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::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::ChunkIterator::try_commit_chunk(Utf8View::Iterator const& start, Utf8View::Iterator const& end, bool has_breaking_newline, Gfx::Font const& font) const +Optional 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::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, }; } diff --git a/Userland/Libraries/LibWeb/Layout/TextNode.h b/Userland/Libraries/LibWeb/Layout/TextNode.h index 72c3b09033e..c6f3d691cfb 100644 --- a/Userland/Libraries/LibWeb/Layout/TextNode.h +++ b/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 next(); + Optional peek(size_t); private: - Optional try_commit_chunk(Utf8View::Iterator const& start, Utf8View::Iterator const& end, bool has_breaking_newline, Gfx::Font const&) const; + Optional next_without_peek(); + Optional 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 m_peek_queue; }; void invalidate_text_for_rendering(); diff --git a/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.cpp b/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.cpp index ced6f483038..c139b6c47f9 100644 --- a/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.cpp +++ b/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) {