diff --git a/src/runtime/roblox_text_surface_overlay.cc b/src/runtime/roblox_text_surface_overlay.cc index 958611b..07cd8d5 100644 --- a/src/runtime/roblox_text_surface_overlay.cc +++ b/src/runtime/roblox_text_surface_overlay.cc @@ -244,7 +244,7 @@ struct TextBytePosition { }; TextBytePosition TextPosition(TTF_Text* layout, std::size_t byte_offset, - int text_width) { + int text_width, bool single_line) { if (layout == nullptr) { return {}; } @@ -259,7 +259,11 @@ TextBytePosition TextPosition(TTF_Text* layout, std::size_t byte_offset, position.y = substring.rect.y; position.height = substring.rect.h; if ((substring.flags & TTF_SUBSTRING_TEXT_END) != 0) { - position.x = std::min(text_width, substring.rect.x + substring.rect.w); + // Trailing whitespace has no ink, so the final cluster rect stops short of + // the laid-out width that a caret at the end of a single line sits behind. + position.x = single_line ? text_width + : std::min(text_width, + substring.rect.x + substring.rect.w); } return position; } @@ -501,11 +505,12 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() { TextBytePosition selection_end_position; if (layout != nullptr) { (void)TTF_GetTextSize(layout, &text_width, &text_height); - caret_position = TextPosition(layout, caret_byte, text_width); - selection_begin_position = - TextPosition(layout, selection_begin_byte, text_width); + caret_position = + TextPosition(layout, caret_byte, text_width, !wrapped_layout); + selection_begin_position = TextPosition(layout, selection_begin_byte, + text_width, !wrapped_layout); selection_end_position = - TextPosition(layout, selection_end_byte, text_width); + TextPosition(layout, selection_end_byte, text_width, !wrapped_layout); } if (!text.value.empty()) { // SDL_ttf implementations have historically differed on whether fg.a is diff --git a/tests/roblox_text_display_state_test.cc b/tests/roblox_text_display_state_test.cc index 5931957..f19b460 100644 --- a/tests/roblox_text_display_state_test.cc +++ b/tests/roblox_text_display_state_test.cc @@ -569,6 +569,31 @@ TEST(RobloxTextDisplayStateTest, WrapsMultilineTextInsideNativeTextBox) { EXPECT_TRUE(overlay.Shutdown().ok()); } +TEST(RobloxTextDisplayStateTest, AdvancesCaretForTrailingSpace) { + RobloxTextSurfaceOverlay overlay; + ASSERT_TRUE(overlay.Initialize({800, 600}).ok()); + RobloxTextDisplaySink sink = overlay.sink(); + const auto rightmost_pixel = [&](const std::string& text, + RobloxTextDisplayEvent event) { + RobloxTextDisplayUpdate update = + Show(1, text, static_cast(text.size())); + update.event = event; + update.font_size = 18.0F; + sink.update(sink.context, update); + MocktailTextOverlayFrameInfo frame; + EXPECT_TRUE(overlay.QueryFrame(&frame)); + std::vector rgba(frame.rgba_bytes); + EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size())); + return FindAlphaBounds(frame, rgba).maximum_x; + }; + + const int typed = rightmost_pixel("hi", RobloxTextDisplayEvent::kShow); + const int spaced = rightmost_pixel("hi ", RobloxTextDisplayEvent::kUpdate); + + EXPECT_GT(spaced, typed); + EXPECT_TRUE(overlay.Shutdown().ok()); +} + } // namespace } // namespace runtime } // namespace mocktail